Compare commits

...

369 Commits

Author SHA1 Message Date
Jonas Letzbor 4ddcf2c143
Add remote transcoding support 2024-03-01 23:27:03 +01:00
Nextcloud bot 3944499365
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-27 02:19:05 +00:00
Nextcloud bot 55ba434019
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-25 02:20:32 +00:00
Nextcloud bot c7751b87e1
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-21 02:17:16 +00:00
Nextcloud bot c475ac7196
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-17 02:26:52 +00:00
Nextcloud bot 8899fdcb34
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-16 02:19:07 +00:00
Nextcloud bot f402aac015
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-14 02:18:53 +00:00
Nextcloud bot 9769afae38
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-11 02:19:42 +00:00
Nextcloud bot 55094b7865
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-04 02:21:21 +00:00
Nextcloud bot 1266b7e559
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-03 02:18:34 +00:00
Nextcloud bot a5a072e836
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-02-01 02:19:50 +00:00
Nextcloud bot 6d785e30fc
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-25 02:17:26 +00:00
Nextcloud bot b044cc2cba
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-21 02:36:50 +00:00
Varun Patil 70b5b83968 Merge branch 'master' of https://github.com/pulsejet/memories 2024-01-20 10:44:05 -08:00
Varun Patil d909eb198f places: fix fileId type (fix #1008)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-20 10:44:04 -08:00
Nextcloud bot 28469f19c3
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-20 02:22:37 +00:00
Varun Patil ecc05f5d2b timeline: improve RAW stacking (#1006)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-19 09:39:38 -08:00
Varun Patil 4bb1f94f35 timeline: handle stacking Pixel 8 Pro RAW stack (fix #1006)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-19 09:33:22 -08:00
Varun Patil 198eb620a0 Merge branch 'master' of https://github.com/pulsejet/memories 2024-01-19 09:23:10 -08:00
Varun Patil e6760c7452 lp: fix support for Ultra HDR motion photos
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-19 09:23:08 -08:00
Nextcloud bot a98b5ceb5d
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-15 02:14:56 +00:00
Nextcloud bot 1e43553eed
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-14 02:18:15 +00:00
Varun Patil 6c9fd552e4 dialog: prevent closing underlying modal
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-10 17:09:53 -08:00
Varun Patil 213a3d3778 edit-meta: refactor datecheck
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-10 16:32:02 -08:00
Varun Patil 37881b36b4 face-edit: fix back button (fix #994)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-10 15:53:07 -08:00
Varun Patil 032397384b v6.2.2 2024-01-10 15:43:01 -08:00
Varun Patil 3addbaeb72 edit-meta: add null check
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-10 15:42:38 -08:00
Varun Patil ea540e1434 edit-meta: remove undef orientation
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-10 15:41:38 -08:00
Varun Patil 3ab5c21970 v6.2.1 2024-01-10 15:24:48 -08:00
Varun Patil 9150e13fa9 dav: fix bug in pipeline
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-10 15:23:35 -08:00
Varun Patil 921345523e edit-meta: skip undefined EXIF
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-10 14:59:20 -08:00
Varun Patil b94275030f v6.2.0 2024-01-09 20:05:06 -08:00
Varun Patil 1c68967454 Update changelog
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-09 20:04:38 -08:00
Varun Patil 0604ee1d14 days: use basename as tie-breaker (#985)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-09 19:55:12 -08:00
Nextcloud bot a071cdbc13
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-08 02:19:54 +00:00
Nextcloud bot 321f387d3f
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-07 02:21:57 +00:00
Varun Patil ca90d0c8d7 docs: add another collation solution (#951)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-05 09:10:06 -08:00
Varun Patil a8a3efd21c config: copy missing from default (fix #971)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-05 09:02:29 -08:00
Varun Patil a8aa090be1 takeout: bump migrator version
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-05 08:19:30 -08:00
Varun Patil a1ee15d288 takeout: include tz (fix #977)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-05 08:18:35 -08:00
Varun Patil 4aaf6c32a2 places-setup: make transaction size configurable (fix #943)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-05 07:59:22 -08:00
Varun Patil 0e42e55333 chore: bump max to 28 (fix #961)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2024-01-05 07:50:02 -08:00
Varun Patil 021b297f0c
Merge pull request #979 from fz72/master
Create metadata for F-Droid repository
2024-01-05 07:44:31 -08:00
Nextcloud bot 79ab5cabc2
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-05 02:22:21 +00:00
Nextcloud bot 4d8174d5e5
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-04 02:20:00 +00:00
Nextcloud bot 301ad38a2a
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-02 02:15:37 +00:00
Nextcloud bot 23ac929048
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-01-01 02:17:55 +00:00
Nextcloud bot 23e1696383
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-31 02:18:35 +00:00
fz72 e6b3b68b99
Rename metadata/en-US/images/screenshot.jpg to metadata/en-US/images/phoneScreenshots/screenshot.jpg 2023-12-30 15:41:51 +00:00
fz72 5d213bdc3b
Add files via upload 2023-12-30 15:41:12 +00:00
fz72 69adef214e
Rename metadata/en-US/icon.png to metadata/en-US/images/icon.png 2023-12-30 15:38:57 +00:00
fz72 c4d727070e
Add files via upload 2023-12-30 15:38:10 +00:00
fz72 f1405b5011
Create full_description.txt 2023-12-30 15:36:08 +00:00
fz72 301e34e0f0
Create short_description.txt 2023-12-30 15:34:25 +00:00
fz72 80565b737a
Create title.txt 2023-12-30 15:33:40 +00:00
fz72 b48ec4337b
add Metadata for F-Droid 2023-12-30 15:32:09 +00:00
Nextcloud bot 44d934d980
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-30 02:25:26 +00:00
Nextcloud bot cd289cf054
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-29 02:16:14 +00:00
Nextcloud bot a856a9d03f
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-28 02:17:54 +00:00
Nextcloud bot a63cedf228
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-27 02:17:04 +00:00
Nextcloud bot 10e481f393
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-26 02:18:25 +00:00
Nextcloud bot c8689351e7
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-25 02:18:51 +00:00
Nextcloud bot a56d3ec1bf
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-24 02:25:00 +00:00
Nextcloud bot c84697d6bb
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-23 02:20:29 +00:00
Nextcloud bot f5627da488
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-20 02:13:57 +00:00
Nextcloud bot 3d147ff29e
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-19 02:15:09 +00:00
Nextcloud bot 7e6ef7a5a9
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-16 02:14:50 +00:00
Nextcloud bot 261d89a501
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-15 02:14:23 +00:00
Nextcloud bot ebd57fe0e6
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-13 02:16:39 +00:00
Nextcloud bot 40fc8299d3
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-12 02:14:59 +00:00
Nextcloud bot 8617c5e32f
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-11 02:20:03 +00:00
Varun Patil ed8e1e4517 Merge branch 'master' of https://github.com/pulsejet/memories 2023-12-09 22:58:10 -08:00
Varun Patil 62a62b453b ios: fix inset padding (fix #957)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-12-09 22:58:06 -08:00
Varun Patil 3165beaacb viewer: fix download of lp video
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-12-09 21:30:07 -08:00
Nextcloud bot 903c96749b
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-08 02:14:57 +00:00
Varun Patil 7b603cd095 ci: always run both static analysis steps 2023-12-02 21:58:28 -08:00
Varun Patil c1a556f235 face: fix back button on merge (fix #949)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-12-02 21:12:24 -08:00
Varun Patil ee0a52d305 Merge branch 'master' of https://github.com/pulsejet/memories 2023-12-02 21:08:59 -08:00
Varun Patil 6b9d5a4566 cluster: fix cursor
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-12-02 21:08:56 -08:00
Nextcloud bot 5bf02246d2
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-03 02:19:21 +00:00
Varun Patil c2958f8127 Merge branch 'master' of https://github.com/pulsejet/memories 2023-12-02 11:27:11 -08:00
Varun Patil ac9ce852bb places: add explicit convert to utf-8 2023-12-02 11:27:08 -08:00
Nextcloud bot db45091379
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-12-01 02:15:04 +00:00
Varun Patil b30786ca0c chore: deps
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-29 12:14:30 -08:00
Varun Patil ec95c4720e face-top: make rename util
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-28 20:42:47 -08:00
Varun Patil 5f138698e0 album: fix route after name edit
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-28 20:25:50 -08:00
Varun Patil 75d82bf2d2 Bump node version
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-27 16:10:07 -08:00
Varun Patil 8c0f3dc8a2 sel: allow both directions for multi-select (fix #893)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-26 20:24:59 -08:00
Varun Patil e69cee9dd4 frame: prevent infinite lp spinner
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-26 19:14:58 -08:00
Varun Patil 2e70655c31 sel: always select clicked photo
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-26 19:10:37 -08:00
Varun Patil 366c6dc5e2 sel: refactor backtracking
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-26 18:51:59 -08:00
Varun Patil 0250b27bdf docs: imporve docstring
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-26 18:18:38 -08:00
Varun Patil 312c98bd70 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-26 18:12:36 -08:00
Varun Patil 1ea70750bf face: adjust top matter rename size
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-26 18:12:31 -08:00
Nextcloud bot 1c8fed779d
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-26 02:21:09 +00:00
Varun Patil 40bcc0e09a v6.1.5 2023-11-25 13:20:11 -08:00
Varun Patil 764505b039 sw: restore origin check
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 13:19:29 -08:00
Varun Patil 81c8fc3049 v6.1.4
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 12:42:02 -08:00
Varun Patil 7c94a4efcc sw: fix static path
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 12:41:22 -08:00
Varun Patil a4ae2c800f v6.1.3 2023-11-25 12:11:43 -08:00
Varun Patil 37b932196f sw: exclude maps
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 12:10:59 -08:00
Varun Patil 012d6981de v6.1.2
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 12:03:48 -08:00
Varun Patil bcca24c5c6 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-25 11:54:42 -08:00
Varun Patil ab24efbeda other: fix error message
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 11:54:31 -08:00
Varun Patil 7b4ad788aa sw: improve strategy
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 11:53:34 -08:00
Varun Patil 1971c5e3ce app: re-enable sw on localhost
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 11:13:25 -08:00
Varun Patil 14702f7669 sw: await update call
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 11:02:50 -08:00
Varun Patil eaba80a73b other: do not block sw
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-25 10:57:03 -08:00
Nextcloud bot 7d093893a3
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-25 02:29:13 +00:00
Varun Patil 5c4a1342e4 docs: add play link at bottom
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 17:10:06 -08:00
Varun Patil 13721398da docs: add discourse link
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 17:07:59 -08:00
Varun Patil 657dae8bac docs: link back to github
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 17:02:05 -08:00
Varun Patil 07965536c2 docs: add edit link
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 16:59:48 -08:00
Varun Patil c832862610 docs: add site url
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 16:56:50 -08:00
Varun Patil 60573a4026 docs: update GH release link for android
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 16:55:00 -08:00
Varun Patil 72aea77927 v6.1.1
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 16:32:06 -08:00
Varun Patil 04ac501477 deps: prevent weird transitive dep usage
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 16:22:54 -08:00
Varun Patil ff912a6bee tw: do not attempt to index zero-byte files (close #933)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 15:06:16 -08:00
Varun Patil eb784ef8ab occ: improve index output
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 14:50:15 -08:00
Varun Patil e5e35ce357 refactor: non-null filters
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 14:16:50 -08:00
Varun Patil 07a20fb454 refactor: truthy filter
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 14:06:38 -08:00
Varun Patil f2f6899d53 dav: improve de-duplication for extend
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 14:01:17 -08:00
Varun Patil 9bc77aeb89 dav: use pipeline for extendWithStack
Related #903

Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 13:56:00 -08:00
Varun Patil 294feef80b exif: write in-place
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 13:18:47 -08:00
Varun Patil 75f7c969de sw: exclude licenses from precache
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 09:32:18 -08:00
Varun Patil 98740af645 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-24 08:58:25 -08:00
Varun Patil 427d8ce920 deps: bump vue
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-24 08:57:23 -08:00
Nextcloud bot ea7689aefa
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-24 02:49:43 +00:00
Nextcloud bot 12054519d1
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-23 02:23:51 +00:00
Varun Patil f148b45fc6 timeline: stack RAW with additional file extension (fix #927)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-22 12:39:36 -08:00
Varun Patil e55f0d6343 timeline: refactor RAW stacking logic
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-22 12:39:08 -08:00
Varun Patil fec834855e frame: reorder raw icon
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-22 12:37:49 -08:00
Varun Patil e3223f3a3f timeline: prevent swipe on scroller (fix #937)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-22 12:02:47 -08:00
Varun Patil 915ee8487d Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-22 11:51:03 -08:00
Varun Patil deb0e5ce16 lp: switch to new tag for 12.70
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-22 11:51:01 -08:00
Nextcloud bot 5f0bc4d363
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-22 02:23:23 +00:00
Varun Patil 9653f01636 cluster: fix outline offset
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-21 13:08:20 -08:00
Varun Patil c96a0e3ed9 bin-ext: bump exiftool
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-21 12:46:00 -08:00
Varun Patil 2bc4837d6f video: improve fallback logic
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-21 11:34:04 -08:00
Varun Patil b9a4be7d20 scroller: give focus back to recycler after interaction
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-21 10:20:25 -08:00
Varun Patil 3fe9fdc363 cluster: allow tab navigation
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-21 10:16:46 -08:00
Varun Patil 7af24512d9 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-21 10:10:55 -08:00
Varun Patil 9cf852b780 timeline: focus on init (fix #932)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-21 10:10:53 -08:00
Nextcloud bot 2d088fc8aa
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-21 02:24:50 +00:00
Varun Patil 35d9df6f9c go-vod/0.2.4
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-20 11:36:29 -08:00
Varun Patil c334c4645d go-vod: update Dockerfile to use jellyfin
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-20 11:35:45 -08:00
Varun Patil 9eed70f848
Merge pull request #929 from szaimen/master
improve aio docs - put the community container first
2023-11-20 11:32:29 -08:00
Simon L 4fa0e236fb put the community container first
Signed-off-by: Simon L <szaimen@e.mail.de>
2023-11-20 14:12:24 +01:00
Nextcloud bot 7bfb3d64b6
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-20 02:24:05 +00:00
Nextcloud bot d885e172cd
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-19 02:27:51 +00:00
Varun Patil 05101ed704 video: change default NVENC scaler to CUDA (#582)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-18 11:28:39 -08:00
Varun Patil a1e6e725a0 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-18 08:59:00 -08:00
Varun Patil 2a3507d5fd share: copy even if can't share (fix #925)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-18 08:56:07 -08:00
Varun Patil 4005a21b2d docs: add cuda scaler to hw
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-17 23:57:30 -08:00
Nextcloud bot 855db3d8c9
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-18 02:32:27 +00:00
Varun Patil 417fdfb862 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-17 10:20:52 -08:00
Varun Patil f38f5e15da index: add verbose logging
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-17 10:20:49 -08:00
Nextcloud bot f547d3edf4
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-17 02:26:20 +00:00
Varun Patil 208939464b docs: fix typo
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-16 15:59:57 -08:00
Varun Patil 2cc31aa567 docs: document AIO community container
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-16 15:57:02 -08:00
Varun Patil 5bcec8a9f6 docs: remove v6 warning
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-16 15:45:47 -08:00
Varun Patil 47ac14ed6d Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-16 08:29:36 -08:00
Varun Patil f8d0a8a0d3 metadata: hide hidden albums
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-16 08:29:34 -08:00
Nextcloud bot 697e29e2ef
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-16 02:52:34 +00:00
Varun Patil 60e080ee64 timeline: fix runaway loader values
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-15 14:11:30 -08:00
Varun Patil eb2263d3ea v6.1.0 2023-11-15 12:31:36 -08:00
Varun Patil 23296931fa Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-15 12:30:43 -08:00
Varun Patil 15aa9fdfd8 readme: add link to releases
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-15 12:30:40 -08:00
Varun Patil 389f98bd60 android/1.6 2023-11-15 12:15:00 -08:00
Varun Patil f2ededa6f4 android: bump min server 2023-11-15 12:14:33 -08:00
Varun Patil b4a5faeb92 ci: skip docs on module change
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-15 12:12:00 -08:00
Varun Patil 0b0301e21e android/1.5 2023-11-15 12:07:41 -08:00
Varun Patil 26417f6d9e bin-ext: bump go-vod
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-15 10:28:12 -08:00
Varun Patil 65a46ab679 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-15 10:26:33 -08:00
Varun Patil d38e2376a5 go-vod/0.2.3
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-15 10:25:59 -08:00
Varun Patil 4a0f9ba183 go-vod: check for root (fix #916)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-15 10:25:23 -08:00
Nextcloud bot 6cd52690e8
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-15 02:29:02 +00:00
Varun Patil 29415b49cf edit-meta: fix GPS ref
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 09:23:13 -08:00
Varun Patil 509f797ffb edit-meta: missing key
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 09:00:25 -08:00
Varun Patil 379184247f image: fix preview race after edit
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 01:31:45 -08:00
Varun Patil 7eb232c10f other: disable service worker in debug mode
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 01:10:29 -08:00
Varun Patil 623cbe5d79 album: fix OG title
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 01:09:59 -08:00
Varun Patil c7ea8ec7bf share: disable link for locals
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 01:02:32 -08:00
Varun Patil 33571bf661 albums: fix link-only titles
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 01:01:01 -08:00
Varun Patil 27f8608d69 albums: hide hidden from list
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 00:52:07 -08:00
Varun Patil 016991d40e docs: update changelog
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 00:41:12 -08:00
Varun Patil 0273ae8537 feat: allow multi-share with sel manager (close #472)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-14 00:40:04 -08:00
Varun Patil f0e1b00096 refactor: image info filling
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-13 23:14:52 -08:00
Varun Patil e396359011 node-share: awits
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-13 23:08:15 -08:00
Varun Patil 14a890796e nx: implement multi-share (fix #901)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-13 22:39:12 -08:00
Varun Patil fe74b9f089 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-13 22:37:22 -08:00
Varun Patil 680322d916 nx: support multi-share API 2023-11-13 22:37:19 -08:00
Varun Patil 29458546b0 node-share: await clipboard writes
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-13 14:15:50 -08:00
Varun Patil fa644b1b70 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-13 11:16:37 -08:00
Varun Patil 89ffdc56a8 node-share: copy on native
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-13 11:16:28 -08:00
Nextcloud bot 98429bfdbb
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-11 02:20:43 +00:00
Varun Patil 18c567bc0e refactor: deprecation fixes
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-09 23:56:00 -08:00
Varun Patil d58c492fac folder: redact hash on route change
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-09 23:51:58 -08:00
Varun Patil 857bcb8773 folder: redact hash on route change
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-09 23:47:09 -08:00
Varun Patil 803628c7a2 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-09 23:43:41 -08:00
Varun Patil c496e0e05a edit-meta: use AllDates for set
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-09 23:43:39 -08:00
Varun Patil 6c42d0b8a4 takeout: fix video dates (fix #910)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-09 23:43:16 -08:00
Nextcloud bot 0e06de2a11
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-10 02:17:26 +00:00
Nextcloud bot 1309c1e217
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-07 02:31:10 +00:00
Nextcloud bot 43ef0510e4
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-06 02:26:22 +00:00
Nextcloud bot e9fd3a528d
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-04 02:25:11 +00:00
Nextcloud bot 6287cdd816
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-03 02:21:50 +00:00
Varun Patil 183de24e62 nx: fix visibility of cursor on video
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-02 13:00:47 -07:00
Varun Patil 7b3119c133 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-02 13:00:17 -07:00
Varun Patil 2c9cdacdfa android: hide status bars on landscape 2023-11-02 13:00:10 -07:00
Varun Patil 90003614b7 timeline: revert loading icon move
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-02 12:12:30 -07:00
Varun Patil a7e7f80745 edit-orientation: add some warnings
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 22:12:39 -07:00
Varun Patil e64d1e8536 Merge branch 'master' of https://github.com/pulsejet/memories 2023-11-01 19:29:44 -07:00
Varun Patil dc4e2ed9f8 refactor(viewer): actions to array
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 19:29:16 -07:00
Nextcloud bot 501ba618e4
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-11-02 02:23:39 +00:00
Varun Patil 60d390517d edit-meta: minor CSS fix
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 19:22:36 -07:00
Varun Patil 8eee97c619 viewer: fix edit meta call
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 18:50:13 -07:00
Varun Patil dc43ecfea7 refactor: remove log statement
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 18:50:00 -07:00
Varun Patil 8c16eecc11 edit-meta: add rotate
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 18:45:24 -07:00
Varun Patil 75237ba505 fix(swipe): z-index on mobile
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 15:13:45 -07:00
Varun Patil 9e7b3a32f6 fix(swipe): animation delay
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 15:03:18 -07:00
Varun Patil 133d167f1a fix(nx): detection on server
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 14:44:38 -07:00
Varun Patil 59ec7119ea timeline: move loading to swipe
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 14:25:50 -07:00
Varun Patil 910cb4ada0 feat(timeline): swipe to refresh (close #547)
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 13:19:48 -07:00
Varun Patil b1edd24dd9 docs: update changelog
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 10:36:44 -07:00
Varun Patil e1dcdb5cab docs: add go-vod badge
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 10:33:44 -07:00
Varun Patil f4d16215f1 ci: exclude android changes
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 10:32:50 -07:00
Varun Patil 3908c6b471 Add 'android/' from commit 'c3e1cb338b8124ed3af6c3ae51ae3927a0086f22'
git-subtree-dir: android
git-subtree-mainline: e95a7e022c
git-subtree-split: c3e1cb338b
2023-11-01 10:29:25 -07:00
Varun Patil e95a7e022c docs: update readme with tag info
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 10:24:20 -07:00
Varun Patil db34e65cb8 docs: update readme
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 10:21:40 -07:00
Varun Patil 6d2ef1cf97 chore: update paths
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 10:14:24 -07:00
Varun Patil 12137fe2a7 go-vod/0.2.2
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 10:08:28 -07:00
Varun Patil 7c905e378f fix(go-vod): output binary name in CI 2023-11-01 10:07:32 -07:00
Varun Patil 8fc4f1058e go-vod/0.2.1 2023-11-01 09:56:06 -07:00
Varun Patil 7acedae06c ci: update go-vod Docker workflow
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 09:52:48 -07:00
Varun Patil 44ad47f5a0 chore: bump go-vod version
Signed-off-by: Varun Patil <radialapps@gmail.com>
2023-11-01 09:50:55 -07:00
Varun Patil c3e1cb338b android: prevent reload on rotate 2023-10-31 23:09:25 -07:00
Varun Patil d71295ff1f v1.4 2023-10-13 23:43:43 -07:00
Varun Patil 69c2e482bc Add self-signed trust for webview 2023-10-13 23:24:53 -07:00
Varun Patil b0c5927d9b Add box for self-signed 2023-10-13 23:12:46 -07:00
Varun Patil 49f97e2895 Move login call to native 2023-10-13 22:06:55 -07:00
Varun Patil dbfd56e727 Bump up version 2023-10-07 09:34:50 -07:00
Varun Patil 4ec42b7e0e Remove opening login toast 2023-10-07 09:13:20 -07:00
Varun Patil 2478dc71fd Check server status code on account 2023-10-07 09:12:54 -07:00
Varun Patil 8b1121fc62 Fix db update events 2023-10-04 15:49:01 -07:00
Varun Patil d2c12316d3 rename variables 2023-10-04 15:35:38 -07:00
Varun Patil 55e5c05d54 Implement BUID 2023-10-04 14:59:47 -07:00
Varun Patil c04cc12e88 Bump version 2023-10-04 12:47:08 -07:00
Varun Patil 0705924227 Pass numbers in JS interface 2023-10-04 12:45:05 -07:00
Varun Patil 4928eef556 remove log message 2023-10-04 12:15:38 -07:00
Varun Patil 728c8e46ed Add threadpool for some ops 2023-10-04 11:46:58 -07:00
Varun Patil 07b4d0dbc4 enable cache 2023-10-03 11:54:39 -07:00
Varun Patil 222db00efa Fix theming 2023-10-03 11:52:15 -07:00
Varun Patil cba4bfc823 no need to specify http in url 2023-10-03 11:22:45 -07:00
Varun Patil 1a4d01f387 Take down req 2023-10-03 11:11:04 -07:00
Varun Patil 9a61e24cee Bump minimum version and check 2023-10-03 11:07:25 -07:00
Varun Patil eb5f998505 Encrypt token (fix #10) 2023-10-03 10:43:21 -07:00
Varun Patil 5ab50fe85d New marking API 2023-10-03 10:07:01 -07:00
Varun Patil d4765fef1a Fix ordering of query responses 2023-10-03 09:06:07 -07:00
Varun Patil 500fe57e49 add serverid api 2023-10-02 18:32:40 -07:00
Varun Patil e3bea8b35b Show sync progress 2023-10-02 13:33:13 -07:00
Varun Patil 5d4fd8b07e Show toast on login 2023-10-02 10:17:27 -07:00
Varun Patil 582035df16 Refactor 2023-10-02 10:09:40 -07:00
Varun Patil 53f8d248b7 Refactor 2023-10-02 09:36:23 -07:00
Varun Patil bb4d204228 Refactor 2023-10-01 20:36:27 -07:00
Varun Patil 9a85fa22ce Add HTTP service 2023-10-01 20:26:50 -07:00
Varun Patil 882a03312f Refactor 2023-10-01 20:19:24 -07:00
Varun Patil 44da3fc059 Refactor 2023-10-01 19:42:37 -07:00
Varun Patil a0c7408086 Refactor 2023-10-01 19:41:26 -07:00
Varun Patil 99dcd129ab Refactor 2023-10-01 19:38:39 -07:00
Varun Patil 6d83c026b1 Refactor 2023-10-01 19:38:19 -07:00
Varun Patil 2f065e6d12 Refactor 2023-10-01 19:37:35 -07:00
Varun Patil 0eee69bacb Refactor 2023-10-01 19:26:11 -07:00
Varun Patil a612b02dfa Refactor 2023-10-01 19:17:05 -07:00
Varun Patil 4e90123814 Refactor 2023-10-01 19:14:43 -07:00
Varun Patil 73ff1b883f Fix iteration 2023-10-01 19:11:44 -07:00
Varun Patil 19e5fa29dc Reorder stuff 2023-10-01 19:04:41 -07:00
Varun Patil 3f85f76d2e Remove dead code 2023-10-01 18:43:38 -07:00
Varun Patil 0796b58b87 Switch share local to AUID 2023-09-30 15:01:35 -07:00
Varun Patil 12d5f137da Switch full load to AUID 2023-09-30 14:47:56 -07:00
Varun Patil f5daa0e557 Adapt to new nx api 2023-09-30 14:15:29 -07:00
Varun Patil 9f466511eb Add dry delete API 2023-09-30 13:39:11 -07:00
Varun Patil 8976b53284 Upgrade gradle 2023-09-30 12:33:12 -07:00
Varun Patil 6b1ccee34c main: remove dead code 2023-08-21 16:08:12 -07:00
Varun Patil ca60375703 reformat manifest 2023-08-21 12:42:38 -07:00
Varun Patil 84086024bd Optimization 2023-08-21 12:41:26 -07:00
Varun Patil 4e89b101b2 Reformat code 2023-08-21 12:38:55 -07:00
Varun Patil 3fb209a994 room: add indices 2023-08-21 12:36:39 -07:00
Varun Patil 6dfe308268 Switch to room 2023-08-21 12:32:52 -07:00
Varun Patil ec8c125b0e account : catch all exceptions 2023-08-21 09:59:22 -07:00
Varun Patil 6f7e68ad17 More refactor 2023-08-21 03:39:16 -07:00
Varun Patil bd5113e6b1 nx: rearrange structure 2023-08-21 03:30:43 -07:00
Varun Patil 23b0f4b102 fields: refactor more 2023-08-21 03:12:41 -07:00
Varun Patil 56a1ec6cf6 Refactor Db calls 2023-08-21 02:42:44 -07:00
Varun Patil 4df353c7d2 Deletion with AUIDs 2023-08-21 00:23:38 -07:00
Varun Patil d9e1c2a95d Implement AUID 2023-08-20 23:08:34 -07:00
Varun Patil 290d8c6bed Store AUID in DB 2023-08-20 21:30:15 -07:00
Varun Patil 831f07a021 Fix perms 2023-08-20 13:55:06 -07:00
Varun Patil 6756d73ffb main: fix color parsing 2023-08-20 13:28:45 -07:00
Varun Patil 1bbc7e5066 update logo 2023-05-26 02:05:29 -07:00
Varun Patil 1e933a0776 Clean up manifest 2023-05-23 20:10:21 -07:00
Varun Patil 7bc13f924f fix wv destruction 2023-05-23 20:09:17 -07:00
Varun Patil 23c784f2cd webview: match host 2023-05-23 20:03:11 -07:00
Varun Patil e5745d0c05 video: support direct playback 2023-05-23 19:57:01 -07:00
Varun Patil 79aecab377 Watch local changes 2023-05-22 19:16:04 -07:00
Varun Patil 76f3a270c3 destroy webview on activity 2023-05-22 18:07:06 -07:00
Varun Patil 056a640fd0 manifest: turn on hw explicitly 2023-05-21 23:29:48 -07:00
Varun Patil 926afdd7b7 manifest: disable backup 2023-05-21 23:29:18 -07:00
Varun Patil 4f660eaaab Change default status color 2023-05-21 23:26:46 -07:00
Varun Patil d35f5b4583 Add current logo 2023-05-21 23:23:08 -07:00
Varun Patil 8341458f80 welcome: more to sync 2023-05-21 21:45:42 -07:00
Varun Patil 21d803bb04 Add local folder to welcome 2023-05-21 21:41:59 -07:00
Varun Patil 39d137a81c Add local folder config 2023-05-21 20:31:30 -07:00
Varun Patil 5355dc5b38 db: reset sync time on version change 2023-05-21 19:26:04 -07:00
Varun Patil d7678fd218 Docs 2023-05-21 12:18:05 -07:00
Varun Patil 94b8fc82d6 Add sync page 2023-05-18 19:50:19 -07:00
Varun Patil 9464a54481 Improve landing page 2023-05-18 19:10:33 -07:00
Varun Patil 7d7a19eb07 Rename variables 2023-05-18 16:34:56 -07:00
Varun Patil 8aba3e7e81 Add nx for logout 2023-05-18 00:00:44 -07:00
Varun Patil e1b58ae6fb Request permission 2023-05-17 23:38:36 -07:00
Varun Patil a4d250168f Fix user agent 2023-05-17 23:17:29 -07:00
Varun Patil 9c83922943 Check min version of Memories 2023-05-17 22:56:42 -07:00
Varun Patil 3626001c4d Check token revocation 2023-05-17 22:41:32 -07:00
Varun Patil 98d7638473 Update UA 2023-05-16 23:34:14 -07:00
Varun Patil 6e10692962 Add login flow 2023-05-16 03:17:45 -07:00
Varun Patil 56308aa8aa nx: add login flow basics 2023-05-16 01:07:00 -07:00
Varun Patil 0a47baf16e Add API call to welcome 2023-05-16 00:25:34 -07:00
Varun Patil 14bbea4c9f Add welcome page 2023-05-15 21:07:36 -07:00
Varun Patil c08d87777a Add delta sync 2023-05-15 20:09:56 -07:00
Varun Patil 2e67ab5cda Minor refactofr 2023-05-15 09:41:05 -07:00
Varun Patil 9488a2bc7d UX improvements 2023-05-14 22:16:11 -07:00
Varun Patil e8c59d7648 Optimizations 2023-05-14 21:56:41 -07:00
Varun Patil 3fb17c0450 exoplayer: button fixes 2023-05-14 19:58:45 -07:00
Varun Patil 15784416eb UX improvements 2023-05-14 19:04:46 -07:00
Varun Patil a2765eef60 Add touch sound api 2023-05-14 17:26:39 -07:00
Varun Patil 4205a65b87 Multiple fixes 2023-05-14 16:25:48 -07:00
Varun Patil d7b550e85a Add HLS playback 2023-05-14 13:57:32 -07:00
Varun Patil 403e4404a7 Add local video playback 2023-05-14 13:32:25 -07:00
Varun Patil ce3ee760d0 Remove nativeX static refs on exit 2023-05-13 17:56:11 -07:00
Varun Patil 7f0a75eae2 refactor 2023-05-13 17:50:40 -07:00
Varun Patil 18817f2642 refactor 2023-05-13 17:46:02 -07:00
Varun Patil c7d2d78619 Fix video deletion 2023-05-13 17:16:39 -07:00
Varun Patil 39f2af8dc3 Convert to kotlin 2023-05-12 01:16:30 -07:00
Varun Patil 94e47a194c Convert some files to kotlin 2023-05-11 23:40:19 -07:00
Varun Patil 5386d72456 dl: allow sharing local video 2023-05-11 21:19:26 -07:00
Varun Patil b0b0b74754 Show local videos in timeline 2023-05-11 21:16:43 -07:00
Varun Patil bc5490ecdb Add local sharing 2023-05-10 20:25:53 -07:00
Varun Patil fb3045d6c9 Remove unused var 2023-05-10 20:04:45 -07:00
Varun Patil 884e70e5e6 Add share API 2023-05-10 20:02:30 -07:00
Varun Patil fb911669e6 tq: delete from table on deletion 2023-05-10 13:25:02 -07:00
Varun Patil 414f6cf5ed Update del API for list 2023-05-10 13:20:31 -07:00
Varun Patil 22ed3b3cb0 Add single delete API 2023-05-10 12:47:26 -07:00
Varun Patil 93ee281eee tq: add fake etag 2023-05-08 21:33:38 -07:00
Varun Patil e8baff4273 dl: filename 2023-05-08 20:30:23 -07:00
Varun Patil 6f11b5eeba add untested download code 2023-05-08 20:02:33 -07:00
Varun Patil a6cf5ad190 info: add EXIF 2023-05-08 15:01:24 -07:00
Varun Patil 5149ef94d4 Add basic image info API 2023-05-08 14:07:12 -07:00
Varun Patil 4aaacdda0d Remove async promise 2023-05-08 13:25:44 -07:00
Varun Patil b23b1d81f6 revert whitespace change 2023-05-08 12:07:14 -07:00
Varun Patil c10f3c1296 Disable cache 2023-05-08 12:06:46 -07:00
Varun Patil 942a1eee2e Add theme color API 2023-05-07 21:07:02 -07:00
Varun Patil 01d4bd8489 Add days API 2023-05-07 20:02:37 -07:00
Varun Patil 93350812a3 tq: remove stale images 2023-05-07 19:08:41 -07:00
Varun Patil cd131797cc Update tq 2023-05-07 18:57:29 -07:00
Varun Patil 535daadc51 Add dayId to DB 2023-05-07 18:26:11 -07:00
Varun Patil af97d312bd Add SQLite DB service 2023-05-07 14:18:25 -07:00
Varun Patil ddd8bb7af6 Add NativeX class 2023-05-07 13:06:12 -07:00
Varun Patil 37415f7dd5 refactor 2023-05-07 13:02:04 -07:00
Varun Patil c9a9e4379b Initial Commit 2023-05-03 20:10:02 -07:00
375 changed files with 10315 additions and 1719 deletions

View File

@ -5,7 +5,6 @@ on:
- master
paths:
- 'docs/**'
- 'go-vod/**'
- 'CHANGELOG.md'
- 'mkdocs.yml'
@ -16,7 +15,7 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:

View File

@ -8,6 +8,7 @@ on:
paths-ignore:
- 'docs/**'
- 'go-vod/**'
- 'android/**'
- 'mkdocs.yml'
- '**.md'
@ -22,10 +23,10 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- name: Checkout the app
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Build vue app
run: |
@ -61,21 +62,21 @@ jobs:
steps:
- name: Checkout server
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: true
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout the app
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: apps/${{ env.APP_NAME }}
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- uses: actions/download-artifact@v2
with:
@ -134,21 +135,21 @@ jobs:
steps:
- name: Checkout server
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: true
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout the app
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: apps/${{ env.APP_NAME }}
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- uses: actions/download-artifact@v2
with:
@ -193,21 +194,21 @@ jobs:
steps:
- name: Checkout server
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: true
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout the app
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: apps/${{ env.APP_NAME }}
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- uses: actions/download-artifact@v2
with:

View File

@ -15,13 +15,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Build
working-directory: go-vod
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs=false -ldflags="-s -w" -o go-vod-amd64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -buildvcs=false -ldflags="-s -w" -o go-vod-arm64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -buildvcs=false -ldflags="-s -w" -o go-vod-aarch64
- name: Upload to releases
uses: svenstaro/upload-release-action@v2
@ -65,6 +65,6 @@ jobs:
platforms: linux/amd64,linux/arm64
context: './go-vod/'
no-cache: true
file: 'Dockerfile'
file: './go-vod/Dockerfile'
tags: radialapps/go-vod:${{ steps.image_label.outputs.label }} , radialapps/go-vod:latest
provenance: false

View File

@ -15,12 +15,12 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- name: Build
run: |

View File

@ -5,12 +5,14 @@ on:
paths-ignore:
- 'docs/**'
- 'go-vod/**'
- 'android/**'
- 'mkdocs.yml'
- '**.md'
pull_request:
paths-ignore:
- 'docs/**'
- 'go-vod/**'
- 'android/**'
- 'mkdocs.yml'
- '**.md'
@ -20,14 +22,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout server
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: true
repository: nextcloud/server
ref: stable27
- name: Checkout the app
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: apps/memories
@ -44,22 +46,24 @@ jobs:
run: |
make install-tools
- name: Run Psalm
working-directory: apps/memories
run: |
vendor/bin/psalm --no-cache --shepherd --stats --threads=max lib
- name: Run PHP-CS-Fixer
if: ${{ ! cancelled() }}
working-directory: apps/memories
run: |
vendor/bin/php-cs-fixer fix --dry-run --diff
- name: Run Psalm
if: ${{ ! cancelled() }}
working-directory: apps/memories
run: |
vendor/bin/psalm --no-cache --shepherd --stats --threads=max lib
vue-lint:
name: Vue Lint
runs-on: ubuntu-latest
steps:
- name: Checkout the app
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci

View File

@ -2,12 +2,35 @@
All notable changes to this project will be documented in this file.
## [Unreleased]
## [v6.2.2] - 2024-01-10
- Hotfix for a bug in request pipelining.
## [v6.2.0] - 2024-01-09
- Nextcloud 28 compatibility
- Various bug fixes
## [v6.1.5] - 2023-11-25
- Hotfix in service worker caching strategy.
## [v6.1.1] - 2023-11-24
- This is an off-cycle hotfix release for some bugs in v6.1.0 ([see](https://github.com/pulsejet/memories/milestone/19?closed=1)).
- **Breaking**: The CUDA scaler is now the default for NVENC. You may need to reconfigure your transcoder. (see [#582](https://github.com/pulsejet/memories/issues/582))
- This release also cuts down a lot of weirdness and improves the usage of dependencies significantly.
## [v6.1.0] - 2023-11-15
- **Feature**: RAW files are now hidden (stacked) when another file with the same basename exists ([#537](https://github.com/pulsejet/memories/issues/537), [#152](https://github.com/pulsejet/memories/issues/152), [#419](https://github.com/pulsejet/memories/issues/419))
- **Feature**: Multiple files can be now selected and shared from the timeline ([#472](https://github.com/pulsejet/memories/issues/472), [#901](https://github.com/pulsejet/memories/issues/901))
- **Feature**: Bulk rotating of images. You can now rotate images losslessly by editing the rotation EXIF metadata. ([#856](https://github.com/pulsejet/memories/issues/856))
- **Feature**: Icon animation when playing live photos ([#898](https://github.com/pulsejet/memories/issues/898))
- **Feature**: Swipe to refresh on timeline ([#547](https://github.com/pulsejet/memories/issues/547))
- **Bugfix**: Allow switching video to direct on Safari ([#650](https://github.com/pulsejet/memories/issues/650))
- Many other [bug fixes](https://github.com/pulsejet/memories/milestone/18?closed=1)
- Android app is now open source ([see](https://github.com/pulsejet/memories/tree/master/android))
## [v6.0.1] - 2023-10-27

View File

@ -14,6 +14,7 @@
[![e2e](https://github.com/pulsejet/memories/actions/workflows/e2e.yaml/badge.svg)](https://github.com/pulsejet/memories/actions/workflows/e2e.yaml)
[![static analysis](https://github.com/pulsejet/memories/actions/workflows/static-analysis.yaml/badge.svg)](https://github.com/pulsejet/memories/actions/workflows/static-analysis.yaml)
[![Shepherd](https://shepherd.dev/github/pulsejet/memories/coverage.svg)](https://shepherd.dev/github/pulsejet/memories)
[![go-vod](https://github.com/pulsejet/memories/actions/workflows/go-vod.yml/badge.svg)](https://github.com/pulsejet/memories/actions/workflows/go-vod.yml)
Memories is a _batteries-included_ photo management solution for Nextcloud with advanced features
@ -41,14 +42,14 @@ Memories is a _batteries-included_ photo management solution for Nextcloud with
## 📱 Mobile Apps
- An Android client for Memories is available in early access on [Google Play](https://play.google.com/store/apps/details?id=gallery.memories).
- An Android client for Memories is available in early access on [Google Play](https://play.google.com/store/apps/details?id=gallery.memories) or [GitHub Releases](https://github.com/pulsejet/memories/releases?q=android).
- For automatic uploads, you can use the official Nextcloud mobile apps.
- Android: [Google Play](https://play.google.com/store/apps/details?id=com.nextcloud.client), [F-Droid](https://f-droid.org/en/packages/com.nextcloud.client/)
- iOS: [App Store](https://apps.apple.com/us/app/nextcloud/id1125420102).
## 🏗 Development Setup
1. ☁ Clone this into your `custom_apps` folder of your Nextcloud.
1. ☁ Clone this monorepo into the `custom_apps` folder of your Nextcloud.
1. 📥 Install [Composer](https://getcomposer.org/) and [Node.js 18](https://nodejs.org)
1. 👩‍💻 In a terminal, run the command `make dev-setup` to install the dependencies.
1. 🏗 To build/watch the UI, run `make watch-js`.
@ -61,6 +62,18 @@ Memories is a _batteries-included_ photo management solution for Nextcloud with
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar): For Vue intellisense and static analysis
- [Volar Typescript](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin): For Vue Typescript support
This monorepo is organized into the following packages:
- [lib](lib): Backend and database migrations (PHP).
- [src](src): Frontend for all platforms (Vue)
- [go-vod](go-vod): On-demand video transcoder (Go)
- [android](android): Android implemention of NativeX (Kotlin)
- [l10n](l10n): Translations (Transifex)
Releases are organized with these tags:
- `v*`: overall releases (e.g. `v1.0.0` or `v1.0.0-beta.1`)
- `go-vod/*`: transcoder releases (e.g. `go-vod/1.0.0`)
- `android/*`: Android releases (e.g. `android/1.0.0`)
## 🤝 Support the project
1. **🌟 Star this repository**: This is the easiest way to support Memories and costs nothing.
@ -85,4 +98,8 @@ For the full changelog, see [CHANGELOG.md](CHANGELOG.md).
To the great folks building Nextcloud, PHP, Vue and all the other dependencies that make this project possible.
Thanks to [GitHub](https://github.com), [CircleCI](https://circleci.com/) and [BrowserStack](https://www.browserstack.com) for sponsorship for Open Source projects for CI / testing on different devices.
Thanks to [GitHub](https://github.com), [CircleCI](https://circleci.com/) and [BrowserStack](https://www.browserstack.com) for sponsorship for Open Source projects for CI / testing on different devices.
## 📄 License
Memories is licensed under the [AGPLv3](COPYING). Subpackages such as [go-vod](go-vod) are licensed under their respective licenses. See the directory of the subpackage for more information.

16
android/.gitignore vendored 100644
View File

@ -0,0 +1,16 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
app/release

3
android/.idea/.gitignore vendored 100644
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1 @@
Memories

View File

@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\varun\.android\avd\Pixel_6_API_33_2.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-05-16T08:14:59.213052500Z" />
</component>
</project>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
</component>
</project>

View File

@ -0,0 +1,14 @@
<project version="4">
<component name="EntryPointsManager">
<list size="1">
<item index="0" class="java.lang.String" itemvalue="android.webkit.JavascriptInterface" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

202
android/LICENSE 100644
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,5 @@
# Memories Android Wrapper
Android implementation of the NativeX interface.
Note that all code under this tree is licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html), unlike Memories itself, which is licensed under the AGPLv3 license.

1
android/app/.gitignore vendored 100644
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,57 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.devtools.ksp' version '1.9.0-1.0.13'
}
android {
namespace 'gallery.memories'
compileSdk 33
defaultConfig {
applicationId "gallery.memories"
minSdk 27
targetSdk 33
versionCode 6
versionName "1.6"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
viewBinding true
}
}
dependencies {
def media_version = "1.1.1"
def room_version = "2.5.2"
implementation 'androidx.core:core-ktx:1.10.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation "androidx.media3:media3-exoplayer:$media_version"
implementation "androidx.media3:media3-ui:$media_version"
implementation "androidx.media3:media3-exoplayer-hls:$media_version"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
ksp "androidx.room:room-compiler:$room_version"
implementation "com.squareup.okhttp3:okhttp:4.10.0"
implementation "io.github.g00fy2:versioncompare:1.5.0"
}

21
android/app/proguard-rules.pro vendored 100644
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# 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

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
<application
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Memories"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Memories.NoActionBar"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".service.DownloadBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="47.276897mm" height="12.685879mm" viewBox="0 0 47.276898 12.685879" version="1.1" id="svg5"
sodipodi:docname="memories-title (1).svg" inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.2378992"
inkscape:cx="85.225033"
inkscape:cy="88.456314"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="1911"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" />
<defs
id="defs2" />
<g
id="layer1"
transform="translate(-57.784684,-63.463599)"
style="stroke:none;stroke-opacity:1;fill:white;fill-opacity:1">
<g
aria-label="Memories"
id="text1574"
style="font-size:14.1111px;line-height:1.25;font-family:monospace;-inkscape-font-specification:monospace;stroke-width:0.264583;stroke:none;stroke-opacity:1;fill:white;fill-opacity:1">
<path
d="m 67.718898,66.850263 c 0,2.328331 -1.467554,4.797774 -1.467554,7.224883 0,1.41111 0.860777,2.074332 1.622776,2.074332 2.031999,0 3.400776,-4.600219 3.781775,-5.870218 0.07056,-0.211667 -0.02822,-0.282222 -0.268111,-0.282222 -0.197555,0 -0.225777,0.05644 -0.296333,0.239889 -0.634999,1.523999 -2.003776,4.938885 -2.850442,4.938885 -0.211666,0 -0.592666,-0.197556 -0.592666,-1.001888 0,-2.356554 1.439332,-4.23333 1.439332,-7.027328 0,-0.719666 -0.141111,-1.79211 -1.142999,-1.79211 -1.326444,0 -2.906887,1.989665 -3.58422,3.443108 0.225778,-0.917221 0.719667,-2.652886 0.719667,-3.711219 0,-0.973666 -0.409222,-1.622776 -1.030111,-1.622776 -1.693332,0 -3.739441,5.26344 -4.388552,6.646328 0.296333,-1.185333 1.298221,-4.684885 1.298221,-5.602107 0,-0.493888 -0.268111,-0.931333 -0.578555,-0.931333 -0.437444,0 -0.917221,0.917222 -1.312332,2.539998 -0.239889,0.945444 -0.649111,3.005665 -0.917222,4.656664 -0.225777,1.396998 -0.366888,2.046109 -0.366888,2.666997 0,0.381 0.183444,0.437445 0.437444,0.437445 0.310444,0 0.536222,-0.08467 0.747888,-0.282222 0.578555,-0.550333 0.620889,-1.622777 0.917222,-2.483554 0.592666,-1.707443 3.062108,-6.180662 3.612441,-6.180662 0.127,0 0.127,0.239889 0.127,0.282222 0,1.636888 -1.241777,4.628441 -1.241777,6.632217 0,0 0,0.409222 0.352778,0.409222 0.155222,0 0.352777,-0.07055 0.522111,-0.197555 0.338666,-0.254 0.409222,-0.592667 0.606777,-1.086555 0.818444,-2.116665 2.82222,-4.571997 3.59833,-4.571997 0.268111,0 0.254,0.239889 0.254,0.451556 z"
style="font-family:'Lofty Goals';-inkscape-font-specification:'Lofty Goals';stroke:none;stroke-opacity:1;fill:white;fill-opacity:1"
id="path7728" />
<path
d="m 71.980438,73.016813 c 1.396999,0 3.033887,-1.693332 3.033887,-2.808109 0,-0.04233 0,-0.07055 -0.01411,-0.112888 -0.07056,-0.08467 -0.155222,-0.141111 -0.268111,-0.141111 -0.366889,0.578555 -1.53811,2.257776 -2.31422,2.257776 -0.437445,0 -0.620889,-0.550333 -0.620889,-0.917222 v -0.07055 c 0.987777,-0.05644 1.91911,-1.298222 1.91911,-2.243665 0,-0.550333 -0.296333,-0.860777 -0.860777,-0.860777 -1.523999,0 -2.314221,1.93322 -2.314221,3.217331 0,0.705555 0.578555,1.67922 1.439332,1.67922 z m 1.030111,-3.880552 c 0,0.395111 -0.719666,1.495777 -1.15711,1.523999 0.02822,-0.381 0.52211,-1.834443 1.001888,-1.834443 0.155222,0 0.155222,0.211666 0.155222,0.310444 z"
style="font-family:'Lofty Goals';-inkscape-font-specification:'Lofty Goals';stroke:none;stroke-opacity:1;fill:white;fill-opacity:1"
id="path7730" />
<path
d="m 75.804523,67.301818 c -1.128888,0 -1.467555,3.838219 -1.467555,4.727218 0,0.352778 0.02822,0.987777 0.508,0.987777 0.790222,0 1.213555,-0.733777 1.425221,-1.396998 0.169333,-0.550333 1.086555,-3.612442 1.552221,-3.668887 0.127,0.197556 0.127,0.635 0.127,0.874889 0,0.888999 -0.08467,1.763887 -0.08467,2.652887 0,0.324555 -0.01411,1.015999 0.465666,1.015999 0.705555,0 1.890887,-3.824108 2.511776,-3.922886 0.07055,0.183444 0.08467,0.366888 0.08467,0.550333 0,1.058332 -0.296333,2.074332 -0.296333,3.132664 0,0.465666 0.05644,1.368777 0.705555,1.368777 0.606777,0 2.610554,-2.892776 2.69522,-3.527775 -0.05644,-0.08467 -0.169333,-0.141111 -0.268111,-0.141111 -0.197555,0 -1.665109,2.342442 -2.046109,2.398887 -0.04233,-0.02822 -0.05644,-0.08467 -0.05644,-0.112889 0,-0.381 0.155223,-0.818444 0.239889,-1.199444 0.169333,-0.761999 0.338667,-1.552221 0.338667,-2.342442 0,-0.508 -0.254,-1.255888 -0.874889,-1.255888 -1.100665,0 -2.017887,1.947332 -2.427109,2.793998 0.01411,-0.578555 0.112889,-1.157111 0.112889,-1.749777 0,-0.564444 -0.05644,-1.66511 -0.846666,-1.66511 -1.171221,0 -2.158998,2.920998 -2.539998,3.83822 0.08467,-0.931333 0.508,-1.820332 0.508,-2.765776 0,-0.282222 0.01411,-0.592666 -0.366889,-0.592666 z"
style="font-family:'Lofty Goals';-inkscape-font-specification:'Lofty Goals';stroke:none;stroke-opacity:1;fill:white;fill-opacity:1"
id="path7732" />
<path
d="m 84.652174,74.046924 c 1.058333,0 1.975554,-1.086555 2.384776,-2.412998 0.860777,-0.09878 2.04611,-0.959555 2.04611,-1.495777 0,-0.127 -0.07056,-0.254 -0.169334,-0.282222 -0.437444,0.352777 -1.199443,0.776111 -1.679221,0.90311 0.02822,-0.183444 0.02822,-0.366888 0.02822,-0.550333 0,-1.396998 -0.747889,-2.652886 -1.594555,-2.652886 -0.691444,0 -1.467554,0.832555 -1.467554,1.566332 0,0.02822 0,0.07055 0,0.09878 -0.691444,0.564444 -1.086555,1.495777 -1.086555,2.539999 0,1.326443 0.649111,2.285998 1.53811,2.285998 z m 1.749777,-3.443109 c -0.606778,-0.338666 -1.284111,-1.044221 -1.284111,-1.467554 0,-0.254 0.268111,-0.550333 0.522111,-0.550333 0.409222,0 0.776111,0.804333 0.776111,1.693332 0,0.112889 0,0.211667 -0.01411,0.324555 z m -0.155223,0.917222 c -0.239888,0.90311 -0.691443,1.679221 -1.227665,1.679221 -0.381,0 -0.733777,-0.620889 -0.733777,-1.298221 0,-0.536222 0.225777,-1.213555 0.465666,-1.523999 0.395111,0.493888 0.959555,0.931332 1.495776,1.142999 z"
style="font-family:'Lofty Goals';-inkscape-font-specification:'Lofty Goals';stroke:none;stroke-opacity:1;fill:white;fill-opacity:1"
id="path7734" />
<path
d="m 89.986155,74.752479 c 1.001888,0 3.570108,-3.697108 3.640664,-4.656663 0.01411,-0.155222 -0.155222,-0.141111 -0.268111,-0.141111 -0.254,0.395111 -2.300109,3.485441 -2.737553,3.485441 -0.09878,0 -0.08467,-0.141111 -0.08467,-0.211666 0.07056,-0.790222 0.705555,-1.594554 0.776111,-2.427109 0.08467,-0.874889 -0.606778,-0.889 -1.312333,-1.128888 0.338667,-0.310445 0.649111,-0.917222 0.691444,-1.354666 0.04233,-0.522111 -0.268111,-1.326443 -0.874888,-1.326443 -1.001888,0 -1.368777,1.015999 -1.439332,1.834443 -0.112889,1.41111 0.931332,1.509887 1.961443,1.834443 -0.01411,0.239888 -0.352778,0.945443 -0.465667,1.227665 -0.225777,0.606778 -0.451555,1.241777 -0.507999,1.876777 -0.04233,0.507999 0.01411,0.987777 0.620888,0.987777 z m -0.155222,-6.349995 c -0.02822,0.282222 -0.169333,0.931332 -0.381,1.128888 -0.211666,-0.112889 -0.239888,-0.479778 -0.225777,-0.705555 0.02822,-0.183445 0.112889,-0.959555 0.395111,-0.959555 0.197555,0 0.225777,0.395111 0.211666,0.536222 z"
style="font-family:'Lofty Goals';-inkscape-font-specification:'Lofty Goals';stroke:none;stroke-opacity:1;fill:white;fill-opacity:1"
id="path7736" />
<path
d="m 94.473479,67.555818 c 0.479778,0 0.719666,-0.550333 0.719666,-0.959555 0,-0.282222 -0.09878,-0.649111 -0.451555,-0.649111 -0.578555,0 -0.804332,0.508 -0.804332,0.945444 0,0.296333 0.211666,0.663222 0.536221,0.663222 z m -0.550333,5.64444 c 0.860778,0 2.568221,-2.328332 2.568221,-3.033887 0,-0.127 -0.02822,-0.211666 -0.169334,-0.211666 -0.52211,0 -1.636887,2.257776 -2.031998,2.257776 -0.211666,0 -0.225778,-0.324556 -0.225778,-0.465667 0,-1.721554 0.677333,-2.356553 0.677333,-2.977442 0,-0.366888 -0.211666,-0.451555 -0.550333,-0.451555 -0.733777,0 -1.213554,1.834443 -1.213554,3.033887 0,0.592666 0.141111,1.848554 0.945443,1.848554 z"
style="font-family:'Lofty Goals';-inkscape-font-specification:'Lofty Goals';stroke:none;stroke-opacity:1;fill:white;fill-opacity:1"
id="path7738" />
<path
d="m 97.098125,73.016813 c 1.396999,0 3.033885,-1.693332 3.033885,-2.808109 0,-0.04233 0,-0.07055 -0.0141,-0.112888 -0.0706,-0.08467 -0.155222,-0.141111 -0.268111,-0.141111 -0.366888,0.578555 -1.53811,2.257776 -2.31422,2.257776 -0.437444,0 -0.620889,-0.550333 -0.620889,-0.917222 v -0.07055 c 0.987777,-0.05644 1.91911,-1.298222 1.91911,-2.243665 0,-0.550333 -0.296333,-0.860777 -0.860777,-0.860777 -1.523999,0 -2.31422,1.93322 -2.31422,3.217331 0,0.705555 0.578555,1.67922 1.439332,1.67922 z m 1.03011,-3.880552 c 0,0.395111 -0.719666,1.495777 -1.15711,1.523999 0.02822,-0.381 0.522111,-1.834443 1.001888,-1.834443 0.155222,0 0.155222,0.211666 0.155222,0.310444 z"
style="font-family:'Lofty Goals';-inkscape-font-specification:'Lofty Goals';stroke:none;stroke-opacity:1;fill:white;fill-opacity:1"
id="path7740" />
<path
d="m 101.00688,72.043148 c -0.95956,0.733777 -1.693333,1.580443 -1.693333,2.328331 0,0.832555 0.634999,1.439332 1.453443,1.439332 1.27,0 2.65289,-1.213554 2.65289,-2.356553 0,-0.776111 -0.31045,-1.298222 -0.73378,-1.693332 0.94544,-0.578556 1.905,-1.100666 2.37066,-1.523999 0.0282,-0.141111 -0.0705,-0.296333 -0.21166,-0.268111 -0.52211,0.127 -1.651,0.634999 -2.75167,1.326443 -0.67733,-0.465666 -1.35466,-0.77611 -1.35466,-1.382888 0,-0.592666 0.60678,-1.707443 1.28411,-1.707443 0.32455,0 0.55033,0.197556 0.55033,0.592666 0,0.324556 -0.16933,0.620889 -0.16933,0.945444 0,0.183444 0.11289,0.324555 0.29633,0.324555 0.508,0 0.80433,-0.64911 0.80433,-1.072443 0,-0.973666 -0.52211,-1.495777 -1.49577,-1.495777 -1.43934,0 -2.441224,1.185333 -2.441224,2.582332 0,1.086554 0.719664,1.566332 1.439334,1.961443 z m -0.69145,2.116665 c 0,-0.578556 0.59267,-1.171222 1.36878,-1.735666 0.42333,0.268111 0.74789,0.578555 0.74789,1.100666 0,0.522111 -0.74789,1.326443 -1.397,1.326443 -0.42333,0 -0.71967,-0.324555 -0.71967,-0.691443 z"
style="font-family:'Lofty Goals';-inkscape-font-specification:'Lofty Goals';stroke:none;stroke-opacity:1;fill:white;fill-opacity:1"
id="path7742" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,107 @@
:root {
--theme-color: #2b94f0;
--fg-color: white;
}
body {
margin: 0;
padding: 0;
background-color: var(--theme-color);
color: var(--fg-color);
overflow: hidden;
font-family: sans-serif;
}
* {
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
.container {
width: 90vw;
max-width: 800px;
margin: 40px auto;
text-align: center;
}
.animatable {
transition: opacity 0.7s ease-in-out, transform 0.7s ease-in-out;
}
.invisible {
transform: translateY(10px);
opacity: 0;
}
p,
div.p {
color: white;
margin-bottom: 30px;
line-height: 1.5em;
}
.logo {
color: var(--fg-color);
margin-bottom: 30px;
width: 60vw;
max-width: 400px;
}
input.m-input {
width: 80vw;
max-width: 800px;
padding: 10px 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 10px;
background-color: #f9f9f9;
color: #333;
outline: none;
transition: border-color 0.3s ease-in-out;
}
input.m-input:focus {
border-color: #0096ff;
box-shadow: 0 0 4px var(--theme-color);
}
.m-button {
display: inline-block;
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
color: var(--theme-color);
background-color: var(--fg-color);
border: none;
border-radius: 20px;
cursor: pointer;
text-decoration: none;
transition: background-color 0.3s ease-in-out;
}
.m-button:disabled {
background-color: #eee;
color: #aaa;
cursor: not-allowed;
}
.m-button.link {
background-color: unset;
color: var(--fg-color);
}
.login-button {
margin: 10px;
margin-top: 12px;
}
div.trust {
margin-top: 10px;
}
input[type="checkbox"] {
width: 1.5em;
height: 1.5em;
vertical-align: -3px;
}

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Memories</title>
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="main" class="container">
<img src="memories.svg" alt="Memories Logo" class="logo" />
<p id="waiting">
Waiting for login to complete <br />
Keep this page open in the background
</p>
</div>
</body>
</html>

View File

@ -0,0 +1,136 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Memories</title>
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="main" class="container animatable invisible">
<img src="memories.svg" alt="Memories Logo" class="logo" />
<p>
Start organizing and sharing your precious moments. Enter the address of
your Nextcloud server to begin.
</p>
<input
type="url"
id="server-url"
class="m-input"
placeholder="nextcloud.example.com"
/>
<div class="trust">
<label for="trust-all">
<input
type="checkbox"
id="trust-all"
class="m-checkbox"
/>
Disable certificate verification (unsafe)
</label>
</div>
<button class="m-button login-button" id="login">
Continue to Login
</button>
<br />
<a class="m-button link" href="https://memories.gallery/install/">
I don't have a server
</a>
</div>
<script>
const urlBox = document.getElementById("server-url");
const loginButton = document.getElementById("login");
function validateUrl(url) {
try {
url = new URL(url);
const protoOk = url.protocol === "http:" || url.protocol === "https:";
const hostOk = url.hostname.length > 0;
return protoOk && hostOk;
} catch (e) {
return false;
}
}
function getUrl() {
const url = urlBox.value.toLowerCase();
if (!url.startsWith("http://") && !url.startsWith("https://")) {
return "https://" + url;
}
return url;
}
function updateLoginEnabled() {
loginButton.disabled = !validateUrl(getUrl());
}
function getMemoriesUrl() {
const url = new URL(getUrl());
// Add trailing slash to the path if it's not there already
if (!url.pathname.endsWith("/")) {
url.pathname += "/";
}
// Add index.php to the path if it's not there already
if (!url.pathname.includes("index.php")) {
url.pathname += "index.php/";
}
// Add path to memories
url.pathname += "apps/memories/";
return url;
}
// Update login button enabled state when the URL changes
urlBox.addEventListener("input", updateLoginEnabled);
updateLoginEnabled();
// Login button click handler
loginButton.addEventListener("click", async () => {
try {
urlBox.disabled = true;
loginButton.disabled = true;
// Abort request after 5 seconds
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
// Login signal
const encUrl = encodeURIComponent(encodeURIComponent(getMemoriesUrl().toString()));
// Trust all certificates
const trustAll = document.getElementById("trust-all").checked ? "1" : "0";
await fetch(`http://127.0.0.1/api/login/${encUrl}?trustAll=${trustAll}`, {
method: "GET",
signal: controller.signal,
});
// API is fine, redirect to login page
clearTimeout(timeoutId);
} catch (e) {
// unreachable?
} finally {
urlBox.disabled = false;
loginButton.disabled = false;
}
});
// Set action bar color
const themeColor = getComputedStyle(
document.documentElement
).getPropertyValue("--theme-color");
globalThis.nativex?.setThemeColor(themeColor, true);
// Make container visible
document.getElementById("main").classList.remove("invisible");
</script>
</body>
</html>

View File

@ -0,0 +1,442 @@
package gallery.memories
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import android.webkit.CookieManager
import android.webkit.SslErrorHandler
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import gallery.memories.databinding.ActivityMainBinding
import java.util.concurrent.Executors
@UnstableApi
class MainActivity : AppCompatActivity() {
companion object {
val TAG = MainActivity::class.java.simpleName
}
val binding by lazy(LazyThreadSafetyMode.NONE) {
ActivityMainBinding.inflate(layoutInflater)
}
val threadPool = Executors.newFixedThreadPool(4)
private lateinit var nativex: NativeX
private var player: ExoPlayer? = null
private var playerUris: Array<Uri>? = null
private var playerUid: Long? = null
private var playWhenReady = true
private var mediaItemIndex = 0
private var playbackPosition = 0L
private var mNeedRefresh = false
private val memoriesRegex = Regex("/apps/memories/.*$")
private var host: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// Restore last known look
restoreTheme()
// Initialize services
nativex = NativeX(this)
// Sync if permission is available
nativex.doMediaSync(false)
// Load JavaScript
initializeWebView()
// Destroy video after 1 seconds (workaround for video not showing on first load)
binding.videoView.postDelayed({
binding.videoView.alpha = 1.0f
binding.videoView.visibility = View.GONE
}, 1000)
}
override fun onDestroy() {
super.onDestroy()
binding.webview.removeAllViews();
binding.coordinator.removeAllViews()
binding.webview.destroy();
nativex.destroy()
}
override fun onConfigurationChanged(config: Configuration) {
super.onConfigurationChanged(config)
// Hide the status bar in landscape
setFullscreen(config.orientation == Configuration.ORIENTATION_LANDSCAPE)
}
public override fun onResume() {
super.onResume()
if (playerUris != null && player == null) {
initializePlayer(playerUris!!, playerUid!!)
}
if (mNeedRefresh) {
refreshTimeline(true)
}
}
public override fun onPause() {
super.onPause()
}
public override fun onStop() {
super.onStop()
releasePlayer()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
when (keyCode) {
KeyEvent.KEYCODE_BACK -> {
if (binding.webview.canGoBack()) {
binding.webview.goBack()
} else {
finish()
}
return true
}
}
}
return super.onKeyDown(keyCode, event)
}
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
private fun initializeWebView() {
// Intercept local APIs
binding.webview.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val pathMatches = request.url.path?.matches(memoriesRegex) == true
val hostMatches = request.url.host.equals(host)
if (pathMatches && hostMatches) {
return false
}
// Open external links in browser
Intent(Intent.ACTION_VIEW, request.url).apply { startActivity(this) }
return true
}
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return if (request.url.host == "127.0.0.1") {
nativex.handleRequest(request)
} else null
}
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
if (nativex.http.isTrustingAllCertificates) {
handler?.proceed()
} else {
nativex.toast("Failed to load due to SSL error: ${error?.primaryError}", true)
super.onReceivedSslError(view, handler, error)
}
}
}
// Pass through touch events
binding.webview.setOnTouchListener { _, event ->
if (player != null) {
binding.videoView.dispatchTouchEvent(event)
}
false
}
val userAgent =
getString(R.string.ua_app_prefix) + BuildConfig.VERSION_NAME + " " + getString(R.string.ua_chrome)
val webSettings = binding.webview.settings
webSettings.javaScriptEnabled = true
webSettings.javaScriptCanOpenWindowsAutomatically = true
webSettings.allowContentAccess = true
webSettings.domStorageEnabled = true
webSettings.databaseEnabled = true
webSettings.userAgentString = userAgent
webSettings.setSupportZoom(false)
webSettings.builtInZoomControls = false
webSettings.displayZoomControls = false
binding.webview.addJavascriptInterface(nativex, "nativex")
binding.webview.setLayerType(View.LAYER_TYPE_HARDWARE, null)
binding.webview.setBackgroundColor(Color.TRANSPARENT)
// binding.webview.clearCache(true)
// WebView.setWebContentsDebuggingEnabled(true);
// Welcome page or actual app
nativex.account.refreshCredentials()
val isApp = loadDefaultUrl()
// Start version check if loaded account
if (isApp) {
// Do not use the threadPool here since this might block indefinitely
Thread { nativex.account.checkCredentialsAndVersion() }.start()
}
}
fun loadDefaultUrl(): Boolean {
// Load app interface if authenticated
host = nativex.http.loadWebView(binding.webview)
if (host != null) return true
// Load welcome page
binding.webview.loadUrl("file:///android_asset/welcome.html");
return false
}
fun initializePlayer(uris: Array<Uri>, uid: Long) {
if (player != null) {
if (playerUid == uid) return
player?.release()
player = null
}
// Prevent re-creating
playerUris = uris
playerUid = uid
// Build exoplayer
player = ExoPlayer.Builder(this)
.build()
.also { exoPlayer ->
// Bind to player view
binding.videoView.player = exoPlayer
binding.videoView.visibility = View.VISIBLE
binding.videoView.setShowNextButton(false)
binding.videoView.setShowPreviousButton(false)
for (uri in uris) {
// Create media item from URI
val mediaItem = MediaItem.fromUri(uri)
// Check if remote or local URI
if (uri.toString().contains("http")) {
// Add cookies from webview to data source
val cookies = CookieManager.getInstance().getCookie(uri.toString())
val httpDataSourceFactory =
DefaultHttpDataSource.Factory()
.setDefaultRequestProperties(mapOf("cookie" to cookies))
.setAllowCrossProtocolRedirects(true)
val dataSourceFactory =
DefaultDataSource.Factory(this, httpDataSourceFactory)
// Check if HLS source from URI (contains .m3u8 anywhere)
exoPlayer.addMediaSource(
if (uri.toString().contains(".m3u8")) {
HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(mediaItem)
} else {
ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(mediaItem)
}
)
} else {
exoPlayer.setMediaItems(listOf(mediaItem), mediaItemIndex, playbackPosition)
}
}
// Catch errors and fall back to other sources
exoPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
exoPlayer.seekToNext()
exoPlayer.playWhenReady = true
exoPlayer.play()
}
})
// Start the player
exoPlayer.playWhenReady = playWhenReady
exoPlayer.prepare()
}
}
fun destroyPlayer(uid: Long) {
if (playerUid == uid) {
releasePlayer()
// Reset vars
playWhenReady = true
mediaItemIndex = 0
playbackPosition = 0L
playerUris = null
playerUid = null
}
}
private fun releasePlayer() {
player?.let { exoPlayer ->
playbackPosition = exoPlayer.currentPosition
mediaItemIndex = exoPlayer.currentMediaItemIndex
playWhenReady = exoPlayer.playWhenReady
exoPlayer.release()
}
player = null
binding.videoView.visibility = View.GONE
}
/**
* Make the app fullscreen.
*/
private fun setFullscreen(value: Boolean) {
if (value) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.apply {
hide(WindowInsets.Type.statusBars())
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
@Suppress("Deprecation")
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.apply {
show(WindowInsets.Type.statusBars())
}
} else {
@Suppress("Deprecation")
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
}
}
}
/**
* Store a given theme for restoreTheme.
*/
fun storeTheme(color: String?, isDark: Boolean) {
if (color == null) return
getSharedPreferences(getString(R.string.preferences_key), 0).edit()
.putString(getString(R.string.preferences_theme_color), color)
.putBoolean(getString(R.string.preferences_theme_dark), isDark)
.apply()
}
/**
* Restore the last known theme color.
*/
fun restoreTheme() {
val preferences = getSharedPreferences(getString(R.string.preferences_key), 0)
val color = preferences.getString(getString(R.string.preferences_theme_color), null)
val isDark = preferences.getBoolean(getString(R.string.preferences_theme_dark), false)
applyTheme(color, isDark)
}
/**
* Apply a color theme.
*/
fun applyTheme(color: String?, isDark: Boolean) {
if (color == null) return
// Set system bars
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val appearance =
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
window.insetsController?.setSystemBarsAppearance(
if (isDark) 0 else appearance,
appearance
)
} else {
window.decorView.systemUiVisibility =
if (isDark) 0 else View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
}
// Set colors
try {
val parsed = Color.parseColor(color.trim())
window.navigationBarColor = parsed
window.statusBarColor = parsed
} catch (e: Exception) {
Log.w(TAG, "Invalid color: $color")
return
}
}
/**
* Do a soft refresh on the open timeline
*/
fun refreshTimeline(force: Boolean = false) {
runOnUiThread {
// Check webview is loaded
if (binding.webview.url == null) return@runOnUiThread
// Schedule for resume if not active
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) || force) {
mNeedRefresh = false
busEmit("nativex:db:updated")
busEmit("memories:timeline:soft-refresh")
} else {
mNeedRefresh = true
}
}
}
/**
* Emit an event to the nextcloud event bus
*/
fun busEmit(event: String, data: String = "null") {
runOnUiThread {
if (binding.webview.url == null) return@runOnUiThread
binding.webview.evaluateJavascript(
"window._nc_event_bus?.emit('$event', $data)",
null
)
}
}
}

View File

@ -0,0 +1,311 @@
package gallery.memories
import android.net.Uri
import android.util.Log
import android.view.SoundEffectConstants
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.widget.Toast
import androidx.media3.common.util.UnstableApi
import gallery.memories.service.AccountService
import gallery.memories.service.DownloadService
import gallery.memories.service.HttpService
import gallery.memories.service.ImageService
import gallery.memories.service.PermissionsService
import gallery.memories.service.TimelineQuery
import org.json.JSONArray
import java.io.ByteArrayInputStream
import java.net.URLDecoder
@UnstableApi
class NativeX(private val mCtx: MainActivity) {
val TAG = NativeX::class.java.simpleName
private var themeStored = false
val query = TimelineQuery(mCtx)
val image = ImageService(mCtx, query)
val http = HttpService()
val account = AccountService(mCtx, http)
val permissions = PermissionsService(mCtx).register()
init {
dlService = DownloadService(mCtx, query)
}
companion object {
var dlService: DownloadService? = null
}
fun destroy() {
dlService = null
query.destroy()
}
object API {
val LOGIN = Regex("^/api/login/.+$")
val DAYS = Regex("^/api/days$")
val DAY = Regex("^/api/days/\\d+$")
val IMAGE_INFO = Regex("^/api/image/info/\\d+$")
val IMAGE_DELETE = Regex("^/api/image/delete/[0-9a-f]+(,[0-9a-f]+)*$")
val IMAGE_PREVIEW = Regex("^/image/preview/\\d+$")
val IMAGE_FULL = Regex("^/image/full/[0-9a-f]+$")
val SHARE_URL = Regex("^/api/share/url/.+$")
val SHARE_BLOB = Regex("^/api/share/blobs$")
val CONFIG_ALLOW_MEDIA = Regex("^/api/config/allow_media/\\d+$")
}
@JavascriptInterface
fun isNative(): Boolean {
return true
}
@JavascriptInterface
fun setThemeColor(color: String?, isDark: Boolean) {
// Save for getting it back on next start
if (!themeStored && http.isLoggedIn()) {
themeStored = true
mCtx.storeTheme(color, isDark);
}
// Apply the theme
mCtx.runOnUiThread {
mCtx.applyTheme(color, isDark)
}
}
@JavascriptInterface
fun playTouchSound() {
mCtx.runOnUiThread {
mCtx.binding.webview.playSoundEffect(SoundEffectConstants.CLICK)
}
}
@JavascriptInterface
fun toast(message: String, long: Boolean = false) {
mCtx.runOnUiThread {
val duration = if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
Toast.makeText(mCtx, message, duration).show()
}
}
@JavascriptInterface
fun logout() {
account.loggedOut()
}
@JavascriptInterface
fun reload() {
mCtx.runOnUiThread {
mCtx.loadDefaultUrl()
}
}
@JavascriptInterface
fun downloadFromUrl(url: String?, filename: String?) {
if (url == null || filename == null) return;
dlService!!.queue(url, filename)
}
@JavascriptInterface
fun setShareBlobs(objects: String?) {
if (objects == null) return;
dlService!!.setShareBlobs(JSONArray(objects))
}
@JavascriptInterface
fun playVideo(auid: String, fileid: Long, urlsArray: String) {
mCtx.threadPool.submit {
// Get URI of remote videos
val urls = JSONArray(urlsArray)
val list = Array(urls.length()) {
Uri.parse(urls.getString(it))
}
// Get URI of local video
val videos = query.getSystemImagesByAUIDs(arrayListOf(auid))
// Play with exoplayer
mCtx.runOnUiThread {
if (!videos.isEmpty()) {
mCtx.initializePlayer(arrayOf(videos[0].uri), fileid)
} else {
mCtx.initializePlayer(list, fileid)
}
}
}
}
@JavascriptInterface
fun destroyVideo(fileid: Long) {
mCtx.runOnUiThread {
mCtx.destroyPlayer(fileid)
}
}
@JavascriptInterface
fun configSetLocalFolders(json: String?) {
if (json == null) return;
query.localFolders = JSONArray(json)
}
@JavascriptInterface
fun configGetLocalFolders(): String {
return query.localFolders.toString()
}
@JavascriptInterface
fun configHasMediaPermission(): Boolean {
return permissions.hasAllowMedia() && permissions.hasMediaPermission()
}
@JavascriptInterface
fun getSyncStatus(): Int {
return query.syncStatus
}
@JavascriptInterface
fun setHasRemote(auids: String, buids: String, value: Boolean) {
Log.v(TAG, "setHasRemote: auids=$auids, buids=$buids, value=$value")
mCtx.threadPool.submit {
val auidArray = JSONArray(auids)
val buidArray = JSONArray(buids)
query.setHasRemote(
List(auidArray.length()) { auidArray.getString(it) },
List(buidArray.length()) { buidArray.getString(it) },
value
)
}
}
fun handleRequest(request: WebResourceRequest): WebResourceResponse {
val path = request.url.path ?: return makeErrorResponse()
val response = try {
when (request.method) {
"GET" -> {
routerGet(request)
}
"OPTIONS" -> {
WebResourceResponse(
"text/plain",
"UTF-8",
ByteArrayInputStream("".toByteArray())
)
}
else -> {
throw Exception("Method Not Allowed")
}
}
} catch (e: Exception) {
Log.w(TAG, "handleRequest: " + e.message)
makeErrorResponse()
}
// Allow CORS from all origins
response.responseHeaders = mutableMapOf(
"Access-Control-Allow-Origin" to "*",
"Access-Control-Allow-Headers" to "*"
)
// Cache image responses for 7 days
if (path.matches(API.IMAGE_PREVIEW) || path.matches(API.IMAGE_FULL)) {
response.responseHeaders["Cache-Control"] = "max-age=604800"
}
return response
}
@Throws(Exception::class)
private fun routerGet(request: WebResourceRequest): WebResourceResponse {
val path = request.url.path ?: return makeErrorResponse()
val parts = path.split("/").toTypedArray()
return if (path.matches(API.LOGIN)) {
makeResponse(
account.login(
URLDecoder.decode(parts[3], "UTF-8"),
request.url.getBooleanQueryParameter("trustAll", false)
)
)
} else if (path.matches(API.DAYS)) {
makeResponse(query.getDays())
} else if (path.matches(API.DAY)) {
makeResponse(query.getDay(parts[3].toLong()))
} else if (path.matches(API.IMAGE_INFO)) {
makeResponse(query.getImageInfo(parts[4].toLong()))
} else if (path.matches(API.IMAGE_DELETE)) {
makeResponse(
query.delete(
parseIds(parts[4]),
request.url.getBooleanQueryParameter("dry", false)
)
)
} else if (path.matches(API.IMAGE_PREVIEW)) {
makeResponse(image.getPreview(parts[3].toLong()), "image/jpeg")
} else if (path.matches(API.IMAGE_FULL)) {
makeResponse(image.getFull(parts[3]), "image/jpeg")
} else if (path.matches(API.SHARE_URL)) {
makeResponse(dlService!!.shareUrl(URLDecoder.decode(parts[4], "UTF-8")))
} else if (path.matches(API.SHARE_BLOB)) {
makeResponse(dlService!!.shareBlobs())
} else if (path.matches(API.CONFIG_ALLOW_MEDIA)) {
permissions.setAllowMedia(true)
if (permissions.requestMediaPermissionSync()) {
doMediaSync(true) // separate thread
}
makeResponse("done")
} else {
throw Exception("Path did not match any known API route: $path")
}
}
private fun makeResponse(bytes: ByteArray?, mimeType: String?): WebResourceResponse {
return if (bytes != null) {
WebResourceResponse(mimeType, "UTF-8", ByteArrayInputStream(bytes))
} else makeErrorResponse()
}
private fun makeResponse(json: Any): WebResourceResponse {
return makeResponse(json.toString().toByteArray(), "application/json")
}
private fun makeErrorResponse(): WebResourceResponse {
val response = WebResourceResponse(
"application/json",
"UTF-8",
ByteArrayInputStream("{}".toByteArray())
)
response.setStatusCodeAndReasonPhrase(500, "Internal Server Error")
return response
}
private fun parseIds(ids: String): List<String> {
return ids.trim().split(",")
}
fun doMediaSync(forceFull: Boolean) {
if (permissions.hasAllowMedia()) {
// Full sync if this is the first time permission was granted
val fullSync = forceFull || !permissions.hasMediaPermission()
mCtx.threadPool.submit {
// Block for media permission
if (!permissions.requestMediaPermissionSync()) return@submit
// Full sync requested
if (fullSync) query.syncFullDb()
// Run delta sync and register hooks
query.initialize()
}
}
}
}

View File

@ -0,0 +1,49 @@
package gallery.memories.dao
import android.content.Context
import androidx.room.Database
import androidx.room.Room.databaseBuilder
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import gallery.memories.R
import gallery.memories.mapper.Photo
@Database(entities = [Photo::class], version = 34)
abstract class AppDatabase : RoomDatabase() {
abstract fun photoDao(): PhotoDao
companion object {
private val DATABASE_NAME = "memories_room"
@Volatile
private var INSTANCE: AppDatabase? = null
fun get(context: Context): AppDatabase {
if (INSTANCE == null) {
synchronized(AppDatabase::class.java) {
val ctx = context.applicationContext
if (INSTANCE == null) {
INSTANCE = databaseBuilder(ctx, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.addCallback(callbacks(ctx))
.build()
}
}
}
return INSTANCE!!
}
private fun callbacks(ctx: Context): Callback {
return object : Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
super.onDestructiveMigration(db)
// retrigger synchronization whenever database is destructed
ctx.getSharedPreferences(ctx.getString(R.string.preferences_key), 0).edit()
.remove(ctx.getString(R.string.preferences_last_sync_time))
.apply()
}
}
}
}
}

View File

@ -0,0 +1,47 @@
package gallery.memories.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import gallery.memories.mapper.Bucket
import gallery.memories.mapper.Day
import gallery.memories.mapper.Photo
@Dao
interface PhotoDao {
@Query("SELECT 1")
fun ping(): Int
@Query("SELECT dayid, COUNT(local_id) AS count FROM photos WHERE bucket_id IN (:bucketIds) AND has_remote = 0 GROUP BY dayid ORDER BY dayid DESC")
fun getDays(bucketIds: List<String>): List<Day>
@Query("SELECT * FROM photos WHERE dayid=:dayId AND bucket_id IN (:buckets) AND has_remote = 0 ORDER BY date_taken DESC")
fun getPhotosByDay(dayId: Long, buckets: List<String>): List<Photo>
@Query("DELETE FROM photos WHERE local_id IN (:fileIds)")
fun deleteFileIds(fileIds: List<Long>)
@Query("SELECT * FROM photos WHERE local_id IN (:fileIds)")
fun getPhotosByFileIds(fileIds: List<Long>): List<Photo>
@Query("SELECT * FROM photos WHERE auid IN (:auids)")
fun getPhotosByAUIDs(auids: List<String>): List<Photo>
@Query("UPDATE photos SET flag=1")
fun flagAll()
@Query("UPDATE photos SET flag=0 WHERE local_id=:fileId")
fun unflag(fileId: Long)
@Query("DELETE FROM photos WHERE flag=1")
fun deleteFlagged()
@Insert
fun insert(vararg photos: Photo)
@Query("SELECT bucket_id, bucket_name FROM photos GROUP BY bucket_id")
fun getBuckets(): List<Bucket>
@Query("UPDATE photos SET has_remote=:v WHERE auid IN (:auids) OR buid IN (:buids)")
fun setHasRemote(auids: List<String>, buids: List<String>, v: Boolean)
}

View File

@ -0,0 +1,8 @@
package gallery.memories.mapper
import androidx.room.ColumnInfo
data class Bucket(
@ColumnInfo(name = "bucket_id") val id: String,
@ColumnInfo(name = "bucket_name") val name: String,
)

View File

@ -0,0 +1,8 @@
package gallery.memories.mapper
import androidx.room.ColumnInfo
data class Day(
@ColumnInfo(name = "dayid") val dayId: Long,
@ColumnInfo(name = "count") val count: Long
)

View File

@ -0,0 +1,59 @@
package gallery.memories.mapper
import androidx.exifinterface.media.ExifInterface
class Fields {
object Day {
const val DAYID = Photo.DAYID
const val COUNT = "count"
}
object Photo {
const val FILEID = "fileid"
const val BASENAME = "basename"
const val MIMETYPE = "mimetype"
const val HEIGHT = "h"
const val WIDTH = "w"
const val SIZE = "size"
const val ETAG = "etag"
const val DATETAKEN = "datetaken"
const val EPOCH = "epoch"
const val AUID = "auid"
const val BUID = "buid"
const val DAYID = "dayid"
const val ISVIDEO = "isvideo"
const val VIDEO_DURATION = "video_duration"
const val EXIF = "exif"
const val PERMISSIONS = "permissions"
}
object Perm {
const val DELETE = "D"
}
object EXIF {
val MAP = mapOf(
ExifInterface.TAG_APERTURE_VALUE to "Aperture",
ExifInterface.TAG_FOCAL_LENGTH to "FocalLength",
ExifInterface.TAG_F_NUMBER to "FNumber",
ExifInterface.TAG_SHUTTER_SPEED_VALUE to "ShutterSpeed",
ExifInterface.TAG_EXPOSURE_TIME to "ExposureTime",
ExifInterface.TAG_ISO_SPEED to "ISO",
ExifInterface.TAG_DATETIME_ORIGINAL to "DateTimeOriginal",
ExifInterface.TAG_OFFSET_TIME_ORIGINAL to "OffsetTimeOriginal",
ExifInterface.TAG_GPS_LATITUDE to "GPSLatitude",
ExifInterface.TAG_GPS_LONGITUDE to "GPSLongitude",
ExifInterface.TAG_GPS_ALTITUDE to "GPSAltitude",
ExifInterface.TAG_MAKE to "Make",
ExifInterface.TAG_MODEL to "Model",
ExifInterface.TAG_ORIENTATION to "Orientation",
ExifInterface.TAG_IMAGE_DESCRIPTION to "Description"
)
}
object Bucket {
const val ID = "id"
const val NAME = "name"
const val ENABLED = "enabled"
}
}

View File

@ -0,0 +1,32 @@
package gallery.memories.mapper
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "photos", indices = [
Index(value = ["local_id"]),
Index(value = ["auid"]),
Index(value = ["buid"]),
Index(value = ["dayid"]),
Index(value = ["flag"]),
Index(value = ["bucket_id"]),
Index(value = ["bucket_id", "dayid", "has_remote"])
]
)
data class Photo(
@PrimaryKey(autoGenerate = true) val id: Int? = null,
@ColumnInfo(name = "local_id") val localId: Long,
@ColumnInfo(name = "auid") val auid: String,
@ColumnInfo(name = "buid") val buid: String,
@ColumnInfo(name = "mtime") val mtime: Long,
@ColumnInfo(name = "date_taken") val dateTaken: Long,
@ColumnInfo(name = "dayid") val dayId: Long,
@ColumnInfo(name = "basename") val baseName: String,
@ColumnInfo(name = "bucket_id") val bucketId: Long,
@ColumnInfo(name = "bucket_name") val bucketName: String,
@ColumnInfo(name = "has_remote") val hasRemote: Boolean,
@ColumnInfo(name = "flag") val flag: Int
)

View File

@ -0,0 +1,12 @@
package gallery.memories.mapper
import org.json.JSONObject
class Response {
companion object {
val OK
get(): JSONObject {
return JSONObject().put("message", "ok")
}
}
}

View File

@ -0,0 +1,262 @@
package gallery.memories.mapper
import android.content.ContentUris
import android.content.Context
import android.icu.text.SimpleDateFormat
import android.icu.util.TimeZone
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import org.json.JSONObject
import java.io.IOException
import java.math.BigInteger
import java.security.MessageDigest
class SystemImage {
var fileId = 0L
var baseName = ""
var mimeType = ""
var dateTaken = 0L
var height = 0L
var width = 0L
var size = 0L
var mtime = 0L
var dataPath = ""
var bucketId = 0L
var bucketName = ""
var isVideo = false
var videoDuration = 0L
val uri: Uri
get() {
return ContentUris.withAppendedId(mCollection, fileId)
}
private var mCollection: Uri = IMAGE_URI
companion object {
val TAG = SystemImage::class.java.simpleName
val IMAGE_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val VIDEO_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
/**
* Iterate over all images/videos in the given collection
* @param ctx Context - application context
* @param collection Uri - either IMAGE_URI or VIDEO_URI
* @param selection String? - selection string
* @param selectionArgs Array<String>? - selection arguments
* @param sortOrder String? - sort order
* @return Sequence<SystemImage>
*/
fun cursor(
ctx: Context,
collection: Uri,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
) = sequence {
// Base fields common for videos and images
val projection = arrayListOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.ORIENTATION,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
)
// Add video-specific fields
if (collection == VIDEO_URI) {
projection.add(MediaStore.Video.Media.DURATION)
}
// Get column indices
val idColumn = projection.indexOf(MediaStore.Images.Media._ID)
val nameColumn = projection.indexOf(MediaStore.Images.Media.DISPLAY_NAME)
val mimeColumn = projection.indexOf(MediaStore.Images.Media.MIME_TYPE)
val heightColumn = projection.indexOf(MediaStore.Images.Media.HEIGHT)
val widthColumn = projection.indexOf(MediaStore.Images.Media.WIDTH)
val sizeColumn = projection.indexOf(MediaStore.Images.Media.SIZE)
val orientationColumn = projection.indexOf(MediaStore.Images.Media.ORIENTATION)
val dateTakenColumn = projection.indexOf(MediaStore.Images.Media.DATE_TAKEN)
val dateModifiedColumn = projection.indexOf(MediaStore.Images.Media.DATE_MODIFIED)
val dataColumn = projection.indexOf(MediaStore.Images.Media.DATA)
val bucketIdColumn = projection.indexOf(MediaStore.Images.Media.BUCKET_ID)
val bucketNameColumn = projection.indexOf(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
// Query content resolver
ctx.contentResolver.query(
collection,
projection.toTypedArray(),
selection,
selectionArgs,
sortOrder
).use { cursor ->
while (cursor!!.moveToNext()) {
val image = SystemImage()
// Common fields
image.fileId = cursor.getLong(idColumn)
image.baseName = cursor.getString(nameColumn)
image.mimeType = cursor.getString(mimeColumn)
image.height = cursor.getLong(heightColumn)
image.width = cursor.getLong(widthColumn)
image.size = cursor.getLong(sizeColumn)
image.dateTaken = cursor.getLong(dateTakenColumn)
image.mtime = cursor.getLong(dateModifiedColumn)
image.dataPath = cursor.getString(dataColumn)
image.bucketId = cursor.getLong(bucketIdColumn)
image.bucketName = cursor.getString(bucketNameColumn)
image.mCollection = collection
// Swap width/height if orientation is 90 or 270
val orientation = cursor.getInt(orientationColumn)
if (orientation == 90 || orientation == 270) {
image.width = image.height.also { image.height = image.width }
}
// Video specific fields
image.isVideo = collection == VIDEO_URI
if (image.isVideo) {
val durationColumn = projection.indexOf(MediaStore.Video.Media.DURATION)
image.videoDuration = cursor.getLong(durationColumn)
}
// Add to main list
yield(image)
}
}
}
/**
* Get image or video by a list of IDs
* @param ctx Context - application context
* @param ids List<Long> - list of IDs
* @return List<SystemImage>
*/
fun getByIds(ctx: Context, ids: List<Long>): List<SystemImage> {
val selection = MediaStore.Images.Media._ID + " IN (" + ids.joinToString(",") + ")"
val images = cursor(ctx, IMAGE_URI, selection, null, null).toList()
if (images.size == ids.size) return images
return images + cursor(ctx, VIDEO_URI, selection, null, null).toList()
}
}
/**
* JSON representation of the SystemImage.
* This corresponds to IPhoto on the frontend.
*/
val json
get(): JSONObject {
val obj = JSONObject()
.put(Fields.Photo.FILEID, fileId)
.put(Fields.Photo.BASENAME, baseName)
.put(Fields.Photo.MIMETYPE, mimeType)
.put(Fields.Photo.HEIGHT, height)
.put(Fields.Photo.WIDTH, width)
.put(Fields.Photo.SIZE, size)
.put(Fields.Photo.ETAG, mtime.toString())
.put(Fields.Photo.EPOCH, epoch)
if (isVideo) {
obj.put(Fields.Photo.ISVIDEO, 1)
.put(Fields.Photo.VIDEO_DURATION, videoDuration / 1000)
}
return obj
}
/** The epoch timestamp of the image. */
val epoch
get(): Long {
return dateTaken / 1000
}
val exifInterface
get() : ExifInterface? {
if (isVideo) return null
try {
return ExifInterface(dataPath)
} catch (e: Exception) {
Log.w(TAG, "Failed to read EXIF data: " + e.message)
return null
}
}
/** The UTC dateTaken timestamp of the image. */
fun utcDate(exif: ExifInterface?): Long {
// Get EXIF date using ExifInterface if image
if (exif != null) {
try {
val exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME)
?: throw IOException()
val sdf = SimpleDateFormat("yyyy:MM:dd HH:mm:ss")
sdf.timeZone = TimeZone.GMT_ZONE
sdf.parse(exifDate).let {
return it.time / 1000
}
} catch (e: Exception) {
Log.w(TAG, "Failed to read EXIF datetime: " + e.message)
}
}
// No way to get the actual local date, so just assume current timezone
return (dateTaken + TimeZone.getDefault().getOffset(dateTaken).toLong()) / 1000
}
fun auid(): String {
return md5("$epoch$size")
}
fun buid(exif: ExifInterface?): String {
var sfx = "size=$size"
if (exif != null) {
try {
val iuid = exif.getAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID)
?: throw IOException()
sfx = "iuid=$iuid"
} catch (e: Exception) {
Log.w(TAG, "Failed to read EXIF unique ID ($baseName): " + e.message)
}
}
return md5("$baseName$sfx");
}
/**
* The database Photo object corresponding to the SystemImage.
* This should ONLY be used for insertion into the database.
*/
val photo
get(): Photo {
val exif = exifInterface
val dateCache = utcDate(exif)
return Photo(
localId = fileId,
auid = auid(),
buid = buid(exif),
mtime = mtime,
dateTaken = dateCache,
dayId = dateCache / 86400,
baseName = baseName,
bucketId = bucketId,
bucketName = bucketName,
flag = 0,
hasRemote = false
)
}
private fun md5(input: String): String {
val md = MessageDigest.getInstance("MD5")
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
}
}

View File

@ -0,0 +1,224 @@
package gallery.memories.service
import SecureStorage
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.media3.common.util.UnstableApi
import gallery.memories.MainActivity
import gallery.memories.R
import io.github.g00fy2.versioncompare.Version
@UnstableApi
class AccountService(private val mCtx: MainActivity, private val mHttp: HttpService) {
companion object {
val TAG = AccountService::class.java.simpleName
}
private val store = SecureStorage(mCtx)
/**
* Make the first request to log in
* @param url The URL of the Nextcloud server
* @param trustAll Whether to trust all certificates
*/
fun login(url: String, trustAll: Boolean) {
try {
mHttp.build(url, trustAll)
val res = mHttp.getApiDescription()
if (res.code != 200) {
throw Exception("${url}api/describe (status ${res.code})")
}
val body = mHttp.bodyJson(res) ?: throw Exception("Failed to parse API description")
val baseUrl = body.getString("baseUrl")
val loginFlowUrl = body.getString("loginFlowUrl")
loginFlow(baseUrl, loginFlowUrl)
} catch (e: Exception) {
toast("Error: ${e.message}")
throw Exception("Failed to connect to server: ${e.message}")
}
}
/**
* Login to a server
* @param baseUrl The base URL of the server
* @param loginFlowUrl The login flow URL
* @throws Exception If the login flow failed
*/
fun loginFlow(baseUrl: String, loginFlowUrl: String) {
val res = mHttp.postLoginFlow(loginFlowUrl)
// Check if 200 was received
if (res.code != 200) {
throw Exception("Login flow returned a ${res.code} status code. Check your reverse proxy configuration and overwriteprotocol is correct.")
}
// Get body as JSON
val body = mHttp.bodyJson(res) ?: throw Exception("Failed to parse login flow response")
// Parse response body as JSON
val pollObj = body.getJSONObject("poll")
val pollToken = pollObj.getString("token")
val pollUrl = pollObj.getString("endpoint")
val loginUrl = body.getString("login")
// Open login page in browser
mCtx.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(loginUrl)))
// Start polling in background
Thread { pollLogin(pollUrl, pollToken, baseUrl) }.start()
}
/**
* Poll the login flow URL until we get a login token
* @param pollUrl The login flow URL
* @param pollToken The login token
* @param baseUrl The base URL of the server
*/
private fun pollLogin(pollUrl: String, pollToken: String, baseUrl: String) {
mCtx.binding.webview.post {
mCtx.binding.webview.loadUrl("file:///android_asset/waiting.html")
}
var pollCount = 0
while (pollCount < 10 * 60) {
pollCount += 3
// Sleep for 3s
Thread.sleep(3000)
try {
val response = mHttp.getPollLogin(pollUrl, pollToken)
val body = mHttp.bodyJson(response) ?: throw Exception("Failed to parse login flow response")
Log.v(TAG, "pollLogin: Got status code ${response.code}")
// Check status code
if (response.code != 200) {
throw Exception("Failed to poll login flow")
}
val loginName = body.getString("loginName")
val appPassword = body.getString("appPassword")
toast("Logged in, waiting for next page ...")
mCtx.runOnUiThread {
// Save login info (also updates header)
storeCredentials(baseUrl, loginName, appPassword)
// Go to next screen
mHttp.loadWebView(mCtx.binding.webview, "nxsetup")
}
return
} catch (e: Exception) {
continue
}
}
}
/**
* Check if the credentials are valid and the server version is supported
* Makes a toast to the user if something is wrong
*/
fun checkCredentialsAndVersion() {
if (!mHttp.isLoggedIn()) return
try {
val response = mHttp.getApiDescription()
val body = mHttp.bodyJson(response)
// Check status code
if (response.code == 401) {
return loggedOut()
}
// Could not connect to memories
if (response.code == 404) {
return toast(mCtx.getString(R.string.err_no_ver))
}
// Check body
if (body == null || response.code != 200) {
toast(mCtx.getString(R.string.err_no_describe))
return
}
// Get body values
val uid = body.get("uid")
val version = body.getString("version")
// Check UID exists
if (uid.equals(null)) {
return loggedOut()
}
// Check minimum version
if (Version(version) < Version(mCtx.getString(R.string.min_server_version))) {
return toast(mCtx.getString(R.string.err_no_ver))
}
} catch (e: Exception) {
Log.w(TAG, "checkCredentialsAndVersion: ", e)
return
}
}
/**
* Handle a logout. Delete the stored credentials and go back to the login screen.
*/
fun loggedOut() {
toast(mCtx.getString(R.string.err_logged_out))
deleteCredentials()
mCtx.runOnUiThread {
mCtx.loadDefaultUrl()
}
}
/**
* Store the credentials
* @param url The URL to store
* @param user The username to store
* @param password The password to store
*/
fun storeCredentials(url: String, user: String, password: String) {
store.saveCredentials(Credential(
url = url,
trustAll = mHttp.isTrustingAllCertificates,
username = user,
token = password,
))
refreshCredentials()
}
/**
* Delete the stored credentials
*/
fun deleteCredentials() {
store.deleteCredentials()
mHttp.setAuthHeader(null)
mHttp.build(null, false)
}
/**
* Refresh the authorization header
*/
fun refreshCredentials() {
val cred = store.getCredentials() ?: return
mHttp.build(cred.url, cred.trustAll)
mHttp.setAuthHeader(Pair(cred.username, cred.token))
}
/**
* Show a toast on the UI thread
* @param message The message to show
*/
private fun toast(message: String) {
mCtx.runOnUiThread {
Toast.makeText(mCtx, message, Toast.LENGTH_LONG).show()
}
}
}

View File

@ -0,0 +1,33 @@
package gallery.memories.service
import android.content.Context
import gallery.memories.R
class ConfigService(private val mCtx: Context) {
companion object {
private var mEnabledBuckets: List<String>? = null
}
/**
* Get the list of enabled local folders
* @return The list of enabled local folders
*/
var enabledBucketIds: List<String>
get() {
if (mEnabledBuckets != null) return mEnabledBuckets!!
mEnabledBuckets = mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0)
.getStringSet(mCtx.getString(R.string.preferences_enabled_local_folders), null)
?.toList()
?: listOf()
return mEnabledBuckets!!
}
set(value) {
mEnabledBuckets = value
mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0).edit()
.putStringSet(
mCtx.getString(R.string.preferences_enabled_local_folders),
value.toSet()
)
.apply()
}
}

View File

@ -0,0 +1,8 @@
package gallery.memories.service
data class Credential(
var url: String,
var trustAll: Boolean,
var username: String,
var token: String,
)

View File

@ -0,0 +1,16 @@
package gallery.memories.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.media3.common.util.UnstableApi
import gallery.memories.NativeX
@UnstableApi class DownloadBroadcastReceiver : BroadcastReceiver() {
/**
* Callback when download is complete
*/
override fun onReceive(context: Context, intent: Intent) {
NativeX.dlService?.runDownloadCallback(intent)
}
}

View File

@ -0,0 +1,161 @@
package gallery.memories.service
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.webkit.CookieManager
import androidx.appcompat.app.AppCompatActivity
import androidx.collection.ArrayMap
import androidx.media3.common.util.UnstableApi
import org.json.JSONArray
import java.util.concurrent.CountDownLatch
@UnstableApi class DownloadService(private val mActivity: AppCompatActivity, private val query: TimelineQuery) {
private val mDownloads: MutableMap<Long, () -> Unit> = ArrayMap()
private var mShareBlobs: JSONArray? = null
/**
* Callback when download is complete
* @param intent The intent that triggered the callback
*/
fun runDownloadCallback(intent: Intent) {
if (mActivity.isDestroyed) return
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE == intent.action) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)
synchronized(mDownloads) {
mDownloads[id]?.let {
it()
mDownloads.remove(id)
return
}
}
}
}
/**
* Queue a download
* @param url The URL to download
* @param filename The filename to save the download as
* @return The download ID
*/
fun queue(url: String, filename: String): Long {
val uri = Uri.parse(url)
val manager = mActivity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(uri)
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
// Copy all cookies from the webview to the download request
val cookies = CookieManager.getInstance().getCookie(url)
request.addRequestHeader("cookie", cookies)
if (filename != "") {
// Save the file to external storage
request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
"memories/$filename"
)
}
// Start the download
return manager.enqueue(request)
}
/**
* Share a URL as a string
* @param url The URL to share
* @return True if the URL was shared
*/
fun shareUrl(url: String): Boolean {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, url)
mActivity.startActivity(Intent.createChooser(intent, null))
return true
}
/**
* Share the blobs from URLs already set by setShareBlobs
* @return True if the URL was shared
*/
@Throws(Exception::class)
fun shareBlobs(): Boolean {
if (mShareBlobs == null) throw Exception("No blobs to share")
// All URIs to share including remote and local files
val uris = ArrayList<Uri>()
val dlIds = ArrayList<Long>()
// Process all objects to share
for (i in 0 until mShareBlobs!!.length()) {
val obj = mShareBlobs!!.getJSONObject(i)
// If AUID is found, then look for local file
val auid = obj.getString("auid")
if (auid != "") {
val sysImgs = query.getSystemImagesByAUIDs(listOf(auid))
if (sysImgs.isNotEmpty()) {
uris.add(sysImgs[0].uri)
continue
}
}
// Queue a download for remote files
dlIds.add(queue(obj.getString("href"), ""))
}
// Wait for all downloads to complete
val latch = CountDownLatch(dlIds.size)
synchronized(mDownloads) {
for (dlId in dlIds) {
mDownloads.put(dlId, fun() { latch.countDown() })
}
}
latch.await()
// Get the URI of the downloaded file
for (id in dlIds) {
val sUri = getDownloadedFileURI(id) ?: throw Exception("Failed to download file")
uris.add(Uri.parse(sUri))
}
// Create sharing intent
val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_STREAM, uris)
mActivity.startActivity(Intent.createChooser(intent, null))
// Reset the blobs
mShareBlobs = null
return true
}
/**
* Set the blobs to share
* @param objects The blobs to share
*/
fun setShareBlobs(objects: JSONArray) {
mShareBlobs = objects
}
/**
* Get the URI of a downloaded file from download ID
* @param downloadId The download ID
* @return The URI of the downloaded file
*/
private fun getDownloadedFileURI(downloadId: Long): String? {
val downloadManager =
mActivity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query()
query.setFilterById(downloadId)
val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
return cursor.getString(columnIndex)
}
cursor.close()
return null
}
}

View File

@ -0,0 +1,194 @@
package gallery.memories.service
import android.net.Uri
import android.util.Base64
import android.webkit.WebView
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class HttpService {
companion object {
val TAG = HttpService::class.java.simpleName
}
private var client = OkHttpClient()
private var authHeader: String? = null
private var mBaseUrl: String? = null
private var mTrustAll = false
/**
* Check if all certificates are trusted
*/
val isTrustingAllCertificates: Boolean
get() = mTrustAll
/**
* Check if the HTTP service is logged in
*/
fun isLoggedIn(): Boolean {
return authHeader != null
}
/**
* Build the HTTP client
* @param url The URL to use
* @param trustAll Whether to trust all certificates
*/
fun build(url: String?, trustAll: Boolean) {
mBaseUrl = url
mTrustAll = trustAll
client = if (trustAll) {
val trustAllCerts = arrayOf<TrustManager>(
object : X509TrustManager {
@Throws(CertificateException::class)
override fun checkClientTrusted(
chain: Array<X509Certificate>,
authType: String
) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(
chain: Array<X509Certificate>,
authType: String
) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
}
)
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, SecureRandom())
OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier({ hostname, session -> true })
.build()
} else {
OkHttpClient()
}
}
/**
* Set the authorization header
* @param credentials The credentials to use
*/
fun setAuthHeader(credentials: Pair<String, String>?) {
if (credentials != null) {
val auth = "${credentials.first}:${credentials.second}"
authHeader = "Basic ${Base64.encodeToString(auth.toByteArray(), Base64.NO_WRAP)}"
return
}
authHeader = null
}
/**
* Load a webview at the default page
* @param webView The webview to load
* @param subpath The subpath to load
* @return Host URL if authenticated, null otherwise
*/
fun loadWebView(webView: WebView, subpath: String? = null): String? {
// Load app interface if authenticated
if (authHeader != null && mBaseUrl != null) {
var url = mBaseUrl
if (subpath != null) url += subpath
// Get host name
val host = Uri.parse(url).host
// Clear webview history
webView.clearHistory()
// Set authorization header
webView.loadUrl(url!!, mapOf("Authorization" to authHeader))
return host
}
return null
}
/** Get body as JSON Object */
@Throws(Exception::class)
fun bodyJson(response: Response): JSONObject? {
return getBody(response)?.let { JSONObject(it) }
}
/** Get body as JSON array */
@Throws(Exception::class)
fun bodyJsonArray(response: Response): JSONArray? {
return getBody(response)?.let { JSONArray(it) }
}
/** Get a string from the response body */
@Throws(Exception::class)
fun getBody(response: Response): String? {
val body = response.body?.string()
response.body?.close()
return body
}
/** Get the API description request */
@Throws(Exception::class)
fun getApiDescription(): Response {
return runRequest(buildGet("api/describe"))
}
/** Make login flow request */
@Throws(Exception::class)
fun postLoginFlow(loginFlowUrl: String): Response {
return runRequest(
Request.Builder()
.url(loginFlowUrl)
.header("User-Agent", "Memories")
.post("".toRequestBody("application/json".toMediaTypeOrNull()))
.build()
)
}
/** Make login polling request */
@Throws(Exception::class)
fun getPollLogin(pollUrl: String, pollToken: String): Response {
return runRequest(
Request.Builder()
.url(pollUrl)
.post("token=$pollToken".toRequestBody("application/x-www-form-urlencoded".toMediaTypeOrNull()))
.build()
)
}
/** Run a request and get a JSON object */
@Throws(Exception::class)
private fun runRequest(request: Request): Response {
return client.newCall(request).execute()
}
/** Build a GET request */
private fun buildGet(path: String, auth: Boolean = true): Request {
val builder = Request.Builder()
.url(mBaseUrl + path)
.header("User-Agent", "Memories")
.get()
if (auth)
builder.header("Authorization", authHeader ?: "")
return builder.build()
}
}

View File

@ -0,0 +1,70 @@
package gallery.memories.service
import android.content.ContentUris
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.os.Build
import android.provider.MediaStore
import androidx.media3.common.util.UnstableApi
import java.io.ByteArrayOutputStream
@UnstableApi class ImageService(private val mCtx: Context, private val query: TimelineQuery) {
/**
* Get a preview image for a given image ID
* @param id The image ID
* @return The preview image as a JPEG byte array
*/
@Throws(Exception::class)
fun getPreview(id: Long): ByteArray {
val bitmap =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mCtx.contentResolver.loadThumbnail(
ContentUris.withAppendedId(
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
id
),
android.util.Size(2048, 2048),
null
)
} else {
MediaStore.Images.Thumbnails.getThumbnail(
mCtx.contentResolver, id, MediaStore.Images.Thumbnails.FULL_SCREEN_KIND, null
)
?: MediaStore.Video.Thumbnails.getThumbnail(
mCtx.contentResolver, id, MediaStore.Video.Thumbnails.FULL_SCREEN_KIND, null
)
?: throw Exception("Thumbnail not found")
}
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream)
return stream.toByteArray()
}
/**
* Get a full image for a given image ID
* @param id The image ID
* @return The full image as a JPEG byte array
*/
@Throws(Exception::class)
fun getFull(auid: String): ByteArray {
val sysImgs = query.getSystemImagesByAUIDs(listOf(auid))
if (sysImgs.isEmpty()) {
throw Exception("Image not found")
}
val uri = sysImgs[0].uri
val bitmap =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(mCtx.contentResolver, uri))
} else {
MediaStore.Images.Media.getBitmap(mCtx.contentResolver, uri)
?: throw Exception("Image not found")
}
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream)
return stream.toByteArray()
}
}

View File

@ -0,0 +1,80 @@
package gallery.memories.service
import android.os.Build
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.media3.common.util.UnstableApi
import gallery.memories.MainActivity
import gallery.memories.R
import java.util.concurrent.CountDownLatch
@UnstableApi class PermissionsService(private val activity: MainActivity) {
var isGranted: Boolean = false
var latch: CountDownLatch? = null
lateinit var requestPermissionLauncher: ActivityResultLauncher<Array<String>>
fun register(): PermissionsService {
requestPermissionLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
// we need all of these
isGranted = permissions.all { it.value }
// Persist that we have it now
setHasMediaPermission(isGranted)
// Release latch
latch?.countDown()
}
return this
}
/**
* Requests media permission and blocks until it is granted
*/
fun requestMediaPermissionSync(): Boolean {
if (isGranted) return true
// Wait for response
latch = CountDownLatch(1)
// Request media read permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissionLauncher.launch(
arrayOf(
android.Manifest.permission.READ_MEDIA_IMAGES,
android.Manifest.permission.READ_MEDIA_VIDEO,
)
)
} else {
requestPermissionLauncher.launch(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE))
}
latch?.await()
return isGranted
}
fun hasMediaPermission(): Boolean {
return activity.getSharedPreferences(activity.getString(R.string.preferences_key), 0)
.getBoolean(activity.getString(R.string.preferences_has_media_permission), false)
}
private fun setHasMediaPermission(v: Boolean) {
activity.getSharedPreferences(activity.getString(R.string.preferences_key), 0).edit()
.putBoolean(activity.getString(R.string.preferences_has_media_permission), v)
.apply()
}
fun hasAllowMedia(): Boolean {
return activity.getSharedPreferences(activity.getString(R.string.preferences_key), 0)
.getBoolean(activity.getString(R.string.preferences_allow_media), false)
}
fun setAllowMedia(v: Boolean) {
activity.getSharedPreferences(activity.getString(R.string.preferences_key), 0).edit()
.putBoolean(activity.getString(R.string.preferences_allow_media), v)
.apply()
}
}

View File

@ -0,0 +1,95 @@
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.security.keystore.KeyProperties.KEY_ALGORITHM_AES
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT
import android.util.Base64
import gallery.memories.service.Credential
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
class SecureStorage(private val context: Context) {
private val keyStore = KeyStore.getInstance("AndroidKeyStore")
private val keyAlias = "MemoriesKey"
init {
keyStore.load(null)
if (!keyStore.containsAlias(keyAlias)) {
generateNewKey()
}
}
fun saveCredentials(cred: Credential) {
val cipher = getCipher()
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
val encryptedToken = cipher.doFinal(cred.token.toByteArray())
context.getSharedPreferences("credentials", Context.MODE_PRIVATE).edit()
.putString("url", cred.url)
.putBoolean("trustAll", cred.trustAll)
.putString("username", cred.username)
.putString("encryptedToken", Base64.encodeToString(encryptedToken, Base64.DEFAULT))
.putString("iv", Base64.encodeToString(cipher.iv, Base64.DEFAULT))
.apply()
}
fun getCredentials(): Credential? {
val sharedPreferences = context.getSharedPreferences("credentials", Context.MODE_PRIVATE)
val url = sharedPreferences.getString("url", null)
val trustAll = sharedPreferences.getBoolean("trustAll", false)
val username = sharedPreferences.getString("username", null)
val encryptedToken = sharedPreferences.getString("encryptedToken", null)
val ivStr = sharedPreferences.getString("iv", null)
if (url != null && username != null && encryptedToken != null && ivStr != null) {
val cipher = getCipher()
val iv = Base64.decode(ivStr, Base64.DEFAULT)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), IvParameterSpec(iv))
val token = String(cipher.doFinal(Base64.decode(encryptedToken, Base64.DEFAULT)))
return Credential(url, trustAll, username, token)
}
return null
}
fun deleteCredentials() {
context.getSharedPreferences("credentials", Context.MODE_PRIVATE).edit()
.remove("url")
.remove("trustAll")
.remove("encryptedUsername")
.remove("encryptedToken")
.remove("iv")
.apply()
}
private fun generateNewKey() {
val keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM_AES, "AndroidKeyStore")
val keyGenSpec = KeyGenParameterSpec.Builder(
keyAlias,
PURPOSE_ENCRYPT or PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(false) // Change this if needed
.build()
keyGenerator.init(keyGenSpec)
keyGenerator.generateKey()
}
private fun getCipher(): Cipher {
val transformation =
"$KEY_ALGORITHM_AES/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}"
return Cipher.getInstance(transformation)
}
private fun getSecretKey(): SecretKey {
return keyStore.getKey(keyAlias, null) as SecretKey
}
}

View File

@ -0,0 +1,461 @@
package gallery.memories.service
import android.annotation.SuppressLint
import android.app.Activity
import android.database.ContentObserver
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.exifinterface.media.ExifInterface
import androidx.media3.common.util.UnstableApi
import gallery.memories.MainActivity
import gallery.memories.R
import gallery.memories.dao.AppDatabase
import gallery.memories.mapper.Fields
import gallery.memories.mapper.Response
import gallery.memories.mapper.SystemImage
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.time.Instant
import java.util.concurrent.CountDownLatch
@UnstableApi
class TimelineQuery(private val mCtx: MainActivity) {
private val TAG = TimelineQuery::class.java.simpleName
private val mConfigService = ConfigService(mCtx)
// Database
private val mDb = AppDatabase.get(mCtx)
private val mPhotoDao = mDb.photoDao()
// Photo deletion events
var deleting = false
var deleteIntentLauncher: ActivityResultLauncher<IntentSenderRequest>
var deleteCallback: ((ActivityResult?) -> Unit)? = null
// Observers
var imageObserver: ContentObserver? = null
var videoObserver: ContentObserver? = null
var refreshPending: Boolean = false
// Status of synchronization process
// -1 = not started
// >0 = number of files updated
var syncStatus = -1
init {
// Register intent launcher for callback
deleteIntentLauncher =
mCtx.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult? ->
synchronized(this) {
deleteCallback?.let { it(result) }
}
}
}
/**
* Initialize content observers for system store.
* Runs the first sync pass.
*/
fun initialize() {
mPhotoDao.ping()
if (syncDeltaDb() > 0) {
mCtx.refreshTimeline()
}
registerHooks()
}
/**
* Destroy content observers for system store.
*/
fun destroy() {
if (imageObserver != null)
mCtx.contentResolver.unregisterContentObserver(imageObserver!!)
if (videoObserver != null)
mCtx.contentResolver.unregisterContentObserver(videoObserver!!)
}
/**
* Register content observers for system store.
*/
fun registerHooks() {
imageObserver = registerContentObserver(SystemImage.IMAGE_URI)
videoObserver = registerContentObserver(SystemImage.VIDEO_URI)
}
/**
* Register content observer for system store.
* @param uri Content URI
* @return Content observer
*/
private fun registerContentObserver(uri: Uri): ContentObserver {
val observer = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
// Debounce refreshes
synchronized(this@TimelineQuery) {
if (refreshPending) return
refreshPending = true
}
// Refresh after 750ms
Thread {
Thread.sleep(750)
synchronized(this@TimelineQuery) {
refreshPending = false
}
// Check if anything to update
if (syncDeltaDb() == 0 || mCtx.isDestroyed || mCtx.isFinishing) return@Thread
mCtx.refreshTimeline()
}.start()
}
}
mCtx.contentResolver.registerContentObserver(uri, true, observer)
return observer
}
/**
* Get system images by AUIDs
* @param auids List of AUIDs
* @return List of SystemImage
*/
fun getSystemImagesByAUIDs(auids: List<String>): List<SystemImage> {
val photos = mPhotoDao.getPhotosByAUIDs(auids)
if (photos.isEmpty()) return listOf()
return SystemImage.getByIds(mCtx, photos.map { it.localId })
}
/**
* Get the days response for local files.
* @return JSON response
*/
@Throws(JSONException::class)
fun getDays(): JSONArray {
return mPhotoDao.getDays(mConfigService.enabledBucketIds).map {
JSONObject()
.put(Fields.Day.DAYID, it.dayId)
.put(Fields.Day.COUNT, it.count)
}.let { JSONArray(it) }
}
/**
* Get the day response for local files.
* @param dayId Day ID
* @return JSON response
*/
@Throws(JSONException::class)
fun getDay(dayId: Long): JSONArray {
// Get the photos for the day from DB
val photos = mPhotoDao.getPhotosByDay(dayId, mConfigService.enabledBucketIds)
.map { it.localId to it }.toMap()
if (photos.isEmpty()) return JSONArray()
val fileIds = photos.keys.toMutableList()
// Get latest metadata from system table
val response = SystemImage.getByIds(mCtx, fileIds).map { image ->
// Mark file exists
fileIds.remove(image.fileId)
// Add missing fields to JSON
val json = image.json
photos[image.fileId]?.let { photo ->
json.put(Fields.Photo.AUID, photo.auid)
.put(Fields.Photo.BUID, photo.buid)
.put(Fields.Photo.DAYID, dayId)
}
json
}.let { JSONArray(it) }
// Remove files that were not found
mPhotoDao.deleteFileIds(fileIds)
return response
}
/**
* Get the image EXIF info response for local files.
* @param id File ID
* @return JSON response
*/
@Throws(Exception::class)
fun getImageInfo(id: Long): JSONObject {
val photos = mPhotoDao.getPhotosByFileIds(listOf(id))
if (photos.isEmpty()) throw Exception("File not found in database")
// Get image from system table
val images = SystemImage.getByIds(mCtx, listOf(id))
if (images.isEmpty()) throw Exception("File not found in system")
// Get the photo and image
val photo = photos[0]
val image = images[0];
// Augment image JSON with database info
val obj = image.json
.put(Fields.Photo.DAYID, photo.dayId)
.put(Fields.Photo.DATETAKEN, photo.dateTaken)
.put(Fields.Photo.PERMISSIONS, Fields.Perm.DELETE)
try {
val exif = ExifInterface(image.dataPath)
obj.put(Fields.Photo.EXIF, JSONObject().apply {
Fields.EXIF.MAP.forEach { (key, field) ->
put(field, exif.getAttribute(key))
}
})
} catch (e: IOException) {
Log.w(TAG, "Error reading EXIF data for $id")
}
return obj
}
/**
* Delete images from local database and system store.
* @param auids List of AUIDs
* @param dry Dry run (returns whether confirmation will be needed)
* @return JSON response
*/
@Throws(Exception::class)
fun delete(auids: List<String>, dry: Boolean): JSONObject {
synchronized(this) {
if (deleting) throw Exception("Already deleting another set of images")
deleting = true
}
val response = Response.OK
try {
// Get list of file IDs
val sysImgs = getSystemImagesByAUIDs(auids)
// Let the UI know how many files we are deleting
response.put("count", sysImgs.size)
// Let the UI know if we are going to ask for confirmation
response.put("confirms", Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
// Exit if dry or nothing to do
if (dry || sysImgs.isEmpty()) return response
// List of URIs
val uris = sysImgs.map { it.uri }
if (uris.isEmpty()) return Response.OK
// Delete file with media store
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val intent = MediaStore.createTrashRequest(mCtx.contentResolver, uris, true)
deleteIntentLauncher.launch(
IntentSenderRequest.Builder(intent.intentSender).build()
)
// Wait for response
val latch = CountDownLatch(1)
var res: ActivityResult? = null
deleteCallback = fun(result: ActivityResult?) {
res = result
latch.countDown()
}
latch.await()
deleteCallback = null;
// Throw if canceled or failed
if (res == null || res!!.resultCode != Activity.RESULT_OK) {
throw Exception("Delete canceled or failed")
}
} else {
for (uri in uris) {
mCtx.contentResolver.delete(uri, null, null)
}
}
// Delete from database
mPhotoDao.deleteFileIds(sysImgs.map { it.fileId })
// Clear UI cache
mCtx.busEmit("nativex:db:updated")
} finally {
synchronized(this) { deleting = false }
}
return response
}
/**
* Sync local database with system store.
* @param startTime Only sync files modified after this time
* @return Number of updated files
*/
private fun syncDb(startTime: Long): Int {
// Date modified is in seconds, not millis
val syncTime = Instant.now().toEpochMilli() / 1000;
// SystemImage query
var selection: String? = null
var selectionArgs: Array<String>? = null
// Query everything modified after startTime
if (startTime != 0L) {
selection = MediaStore.Images.Media.DATE_MODIFIED + " > ?"
selectionArgs = arrayOf(startTime.toString())
}
// Count number of updates
var updates = 0
try {
// Iterate all images from system store
for (image in SystemImage.cursor(
mCtx,
SystemImage.IMAGE_URI,
selection,
selectionArgs,
null
)) {
insertItemDb(image)
updates++
syncStatus = updates
}
// Iterate all videos from system store
for (video in SystemImage.cursor(
mCtx,
SystemImage.VIDEO_URI,
selection,
selectionArgs,
null
)) {
insertItemDb(video)
updates++
syncStatus = updates
}
// Store last sync time
mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0).edit()
.putLong(mCtx.getString(R.string.preferences_last_sync_time), syncTime)
.apply()
} catch (e: Exception) {
Log.e(TAG, "Error syncing database", e)
}
// Reset sync status
synchronized(this) {
syncStatus = -1
}
// Number of updated files
return updates
}
/**
* Sync local database with system store.
* @return Number of updated files
*/
fun syncDeltaDb(): Int {
// Exit if already running
synchronized(this) {
if (syncStatus != -1) return 0
syncStatus = 0
}
// Get last sync time
val syncTime = mCtx.getSharedPreferences(mCtx.getString(R.string.preferences_key), 0)
.getLong(mCtx.getString(R.string.preferences_last_sync_time), 0L)
return syncDb(syncTime)
}
/**
* Sync local database with system store.
* Runs a full synchronization pass, flagging all files for removal.
* @return Number of updated files
*/
fun syncFullDb() {
// Exit if already running
synchronized(this) {
if (syncStatus != -1) return
syncStatus = 0
}
// Flag all images for removal
mPhotoDao.flagAll()
// Sync all files, marking them in the process
syncDb(0L)
// Clean up stale files
mPhotoDao.deleteFlagged()
}
/**
* Insert item into local database.
* @param image SystemImage
*/
@SuppressLint("SimpleDateFormat")
private fun insertItemDb(image: SystemImage) {
val fileId = image.fileId
val baseName = image.baseName
// Check if file with local_id and mtime already exists
val l = mPhotoDao.getPhotosByFileIds(listOf(fileId))
if (!l.isEmpty() && l[0].mtime == image.mtime) {
// File already exists, remove flag
mPhotoDao.unflag(fileId)
Log.v(TAG, "File already exists: $fileId / $baseName")
return
}
// Convert to photo
val photo = image.photo
// Delete file with same local_id and insert new one
mPhotoDao.deleteFileIds(listOf(fileId))
mPhotoDao.insert(photo)
Log.v(TAG, "Inserted file to local DB: $photo")
}
/**
* Set has_remote for list of AUIDs
* @param auids List of AUIDs
* @param value Value to set
*/
fun setHasRemote(auids: List<String>, buids: List<String>, value: Boolean) {
mPhotoDao.setHasRemote(auids, buids, value)
}
/**
* Active local folders response.
* This is in timeline query because it calls the database service.
*/
var localFolders: JSONArray
get() {
return mPhotoDao.getBuckets().map {
JSONObject()
.put(Fields.Bucket.ID, it.id)
.put(Fields.Bucket.NAME, it.name)
.put(Fields.Bucket.ENABLED, mConfigService.enabledBucketIds.contains(it.id))
}.let { JSONArray(it) }
}
set(value) {
val enabled = mutableListOf<String>()
for (i in 0 until value.length()) {
val obj = value.getJSONObject(i)
if (obj.getBoolean(Fields.Bucket.ENABLED)) {
enabled.add(obj.getString(Fields.Bucket.ID))
}
}
mConfigService.enabledBucketIds = enabled
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/coordinator"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.media3.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:alpha="0.0"
app:show_buffering="always"
/>
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:soundEffectsEnabled="true"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@ -0,0 +1,16 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Memories" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/theme</item>
<item name="colorPrimaryVariant">@color/theme</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">200dp</dimen>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="theme">#2b94f0</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@ -0,0 +1,20 @@
<resources>
<string name="app_name">Memories</string>
<string name="min_server_version">6.1.0</string>
<string name="preferences_key">memories</string>
<string name="preferences_theme_color">themeColor</string>
<string name="preferences_theme_dark">themeDark</string>
<string name="preferences_last_sync_time">lastDbSyncTime</string>
<string name="preferences_has_media_permission">hasMediaPermission</string>
<string name="preferences_allow_media">allowMedia</string>
<string name="preferences_enabled_local_folders">enabledLocalFolders</string>
<!-- https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome -->
<string name="ua_chrome">"Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.76 Mobile Safari/537.36"</string>
<string name="ua_app_prefix">MemoriesNative/</string>
<string name="err_no_ver">Your server does not have the minimum required version of Memories</string>
<string name="err_logged_out">Logged out from server</string>
<string name="err_no_describe">Failed to connect to server. Reset app data if this persists.</string>
</resources>

View File

@ -0,0 +1,25 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Memories" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/theme</item>
<item name="colorPrimaryVariant">@color/theme</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
<style name="Theme.Memories.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.Memories.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.Memories.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,12 @@
buildscript {
ext.kotlin_version = '1.9.0'
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
plugins {
id 'com.android.application' version '8.1.2' apply false
id 'com.android.library' version '8.1.2' apply false
id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
}

View File

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Tue May 02 23:46:18 PDT 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

185
android/gradlew vendored 100644
View File

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
android/gradlew.bat vendored 100644
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,16 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Memories"
include ':app'

View File

@ -29,7 +29,7 @@ Memories is a *batteries-included* photo management solution for Nextcloud with
1. Run `php occ memories:index` to generate metadata indices for existing photos.
1. Open the 📷 Memories app in Nextcloud and set the directory containing your photos.
]]></description>
<version>6.0.1</version>
<version>6.2.2</version>
<licence>agpl</licence>
<author mail="radialapps@gmail.com">Varun Patil</author>
<namespace>Memories</namespace>
@ -39,7 +39,7 @@ Memories is a *batteries-included* photo management solution for Nextcloud with
<repository>https://github.com/pulsejet/memories</repository>
<screenshot>https://raw.githubusercontent.com/pulsejet/memories/master/appinfo/screenshot.jpg</screenshot>
<dependencies>
<nextcloud min-version="26" max-version="27"/>
<nextcloud min-version="26" max-version="28"/>
</dependencies>
<commands>
<command>OCA\Memories\Command\Index</command>

View File

@ -58,7 +58,7 @@ Memories works out-of-the-box with most Nextcloud setups, including with externa
## Transcoding
Memories bundles a [transcoding server](https://github.com/pulsejet/go-vod) with HLS capabilites for adaptive streaming. You need to configure transcoding to be able to play any videos. HLS enables the browser to download the video as small chunks and in resolutions adaptive to the connection speed. As a result, this is usually expected to have a major boost in video experience and performance.
Memories bundles a [transcoding server](https://github.com/pulsejet/memories/tree/master/go-vod) with HLS capabilites for adaptive streaming. You need to configure transcoding to be able to play any videos. HLS enables the browser to download the video as small chunks and in resolutions adaptive to the connection speed. As a result, this is usually expected to have a major boost in video experience and performance.
You can configure transcoding from the admin panel. Make sure to test all settings carefully on different kinds of videos.

View File

@ -47,7 +47,7 @@ Yes. All photos are stored in a folder structure, and only displayed as a flat t
**Does it have a mobile app?**
The Android app is available in early access on [Google Play](https://play.google.com/store/apps/details?id=gallery.memories). The web app is very responsive on mobile and can be used on Android and iOS. You can use the official Nextcloud app to auto-upload photos and videos from your device.
The Android app is available in early access on [Google Play](https://play.google.com/store/apps/details?id=gallery.memories) or [GitHub Releases](https://github.com/pulsejet/memories/releases?q=android). The web app is very responsive on mobile and can be used on Android and iOS. You can use the official Nextcloud app to auto-upload photos and videos from your device.
**How is it better than the `Y` FOSS photo manager?**

View File

@ -26,17 +26,13 @@ NVIDIA GPUs support hardware transcoding using NVENC.
## External Transcoder
!!! success "Recommmended Configuration"
!!! success "Recommended Configuration"
The easiest and recommended way to use hardware transcoding in a docker environment is to use an external transcoder.
This setup utilizes a separate docker container that contains the hardware drivers and ffmpeg.
If you cannot do this, other installation methods are also possible (see below).
If you cannot do this, other installation methods are also possible.
!!! warning "Memories v6+ required"
This method is only supported in Memories v6 and newer. For older versions, see [below](#external-transcoder-v5).
[go-vod](https://github.com/pulsejet/go-vod), the transcoder of Memories, comes with a pre-built Docker image based on `linuxserver/ffmpeg`. The docker image connects to your Nextcloud instance and pulls the go-vod binary on startup. To set up an external transcoder, follow these steps.
[go-vod](https://github.com/pulsejet/memories/tree/master/go-vod), the transcoder of Memories, comes with a pre-built Docker image based on `linuxserver/ffmpeg`. The docker image connects to your Nextcloud instance and pulls the go-vod binary on startup. To set up an external transcoder, follow these steps.
1. Use a `docker-compose.yml` that runs the go-vod container and mounts the Nextcloud data directories to it. You must specify `NEXTCLOUD_HOST` to match the name of your Nextcloud container.
@ -70,7 +66,7 @@ NVIDIA GPUs support hardware transcoding using NVENC.
The `NEXTCLOUD_HOST` environment variable must be set to the URL of your Nextcloud instance. If you are using a reverse proxy, you must set this to the URL of the reverse proxy. If you are using a self-signed certificate or http, you must also set `NEXTCLOUD_ALLOW_INSECURE=1`. This URL is used to download the transcoder binary and to connect to the Nextcloud instance.
!!! tip "Setup for NVENC"
If you want to use NVENC instead of VA-API, uncomment the `runtime` line and remove the `devices` section above. You will need to install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) on your host.
If you want to use NVENC instead of VA-API, uncomment the `runtime` line and remove the `devices` section above. You will need to install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) on your host. You may also need to switch to the CUDA scaler in the Memories admin panel.
1. You can now configure the go-vod connect address in the Memories admin panel to point to the external container. go-vod uses port `47788` by default, so in our example the **connection address** would be set to **`go-vod:47788`**.
@ -78,50 +74,16 @@ NVIDIA GPUs support hardware transcoding using NVENC.
Your external transcoder should now be functional. You can check the transcoding logs by running `docker compose logs -f go-vod`.
!!! info "Usage with Nextcloud AIO"
!!! tip "Usage with Nextcloud AIO"
With Nextcloud AIO, you will need to put the container into the `nextcloud-aio` network. Also the `datadir` of AIO needs to be mounted at the same place like in its Netxcloud container into the go-vod container. Usually this would be `nextcloud_aio_nextcloud_data:/mnt/ncdata:ro` or `$NEXTCLOUD_DATADIR:/mnt/ncdata:ro`.
See the instructions [here](https://github.com/nextcloud/all-in-one#how-to-enable-hardware-transcoding-for-nextcloud).
If you are not using NVENC, you can use the **memories community container**. Relevant documentation can be found [here](https://github.com/nextcloud/all-in-one/tree/main/community-containers/memories), and general directions on using community containers [here](https://github.com/nextcloud/all-in-one/tree/main/community-containers). AIO v7.7.0 or higher is required.
Otherwise, if you want to use NVENC with AIO, you will need to put the container into the `nextcloud-aio` network. Also the `datadir` of AIO needs to be mounted at the same place as in its Nextcloud container into the go-vod container. Usually this would be `nextcloud_aio_nextcloud_data:/mnt/ncdata:ro` or `$NEXTCLOUD_DATADIR:/mnt/ncdata:ro`.
!!! info "Usage without Docker Compose"
You can run a similar setup without `docker-compose`. Make sure that the Nextcloud and go-vod containers are in the same network and that the Nextcloud data directories are mounted at the same locations in both containers.
## External Transcoder (v5)
!!! danger "Deprecated"
Use this method if you're running a version of Memories v5 or older. For newer versions, see [above](#external-transcoder).
[go-vod](https://github.com/pulsejet/go-vod), the transcoder of Memories, ships with a Dockerfile that already includes the latest ffmpeg and VA-API drivers. To set up an external transcoder, follow these steps.
1. Clone the go-vod repository. Make sure you use the correct tag, which can be found in the admin panel. Note that this is **not** the same as the version of Memories you run.
```bash
git clone -b <tag> https://github.com/pulsejet/go-vod
```
!!! tip "go-vod version"
Make sure you always use the correct version of go-vod corresponding to your Memories installation. If you use a different version, the admin panel will show a warning and transcoding may not work properly.
1. Use a `docker-compose` file that builds the go-vod container and mounts the Nextcloud data directories to it. The directory containing the `docker-compose.yml` must contain the `go-vod` repository in it. You can then run `docker compose build` to build the image and `docker compose up -d` to start the containers.
```yaml
# docker-compose.yml
services:
server:
image: nextcloud
volumes:
- ncdata:/var/www/html
go-vod:
build: ./go-vod
restart: always
devices:
- /dev/dri:/dev/dri
volumes:
- ncdata:/var/www/html:ro
```
## Internal Transcoder
Memories ships with an internal transcoder binary that you can directly use. In this case, you must install the drivers and ffmpeg on the same host as Nextcloud, and Memories will automatically handle starting and communicating with go-vod. This is also the default setup when you enable transcoding without hardware acceleration.
@ -134,7 +96,7 @@ Memories ships with an internal transcoder binary that you can directly use. In
!!! tip "NVENC"
These instructions mostly focus on VA-API. For NVENC, you may find further useful
pointers in [this](https://github.com/pulsejet/go-vod/blob/master/build-ffmpeg-nvidia.sh) build script.
pointers in [this](https://github.com/pulsejet/memories/blob/master/go-vod/build-ffmpeg-nvidia.sh) build script.
### Bare Metal
@ -178,7 +140,7 @@ sudo -u www-data \
!!! warning "Beware of old ffmpeg and driver versions"
Some package repositories distribute old ffmpeg versions that do not support some modern hardware. (e.g., the VA-API driver installed by `apt` in the current debian image used by Nextcloud only supports up to 10th generation Intel Ice Lake CPUs). To ensure you have a compatible version, you may want to remove your existing ffmpeg version and build the drivers and ffmpeg from source. [This script](https://github.com/pulsejet/go-vod/blob/master/build-ffmpeg.sh) for VA-API or [this one](https://github.com/pulsejet/go-vod/blob/master/build-ffmpeg-nvidia.sh) for NVENC might be useful.
Some package repositories distribute old ffmpeg versions that do not support some modern hardware. (e.g., the VA-API driver installed by `apt` in the current debian image used by Nextcloud only supports up to 10th generation Intel Ice Lake CPUs). To ensure you have a compatible version, you may want to remove your existing ffmpeg version and build the drivers and ffmpeg from source. [This script](https://github.com/pulsejet/memories/blob/master/go-vod/build-ffmpeg.sh) for VA-API or [this one](https://github.com/pulsejet/memories/blob/master/go-vod/build-ffmpeg-nvidia.sh) for NVENC might be useful.
### Docker

View File

@ -14,7 +14,7 @@ For the best experience, we recommend to use the latest stable version of Nextcl
For easy setup and maintenance, you can use the community Nextcloud Docker image, and add extra dependencies using a custom Dockerfile.
Another option is to use [Nextcloud AIO](https://github.com/nextcloud/all-in-one#how-to-use-this), in which case most dependencies are already installed.
!!! success "Recommmended Configuration"
!!! success "Recommended Configuration"
If you plan to use hardware transcoding, using **Docker Compose** or **Nextcloud AIO** is recommended.
@ -49,6 +49,6 @@ To build the app from source, you need to have [node.js](https://nodejs.org/) in
## Mobile Apps
An Android client for Memories is available in early access on [Google Play](https://play.google.com/store/apps/details?id=gallery.memories).
An Android client for Memories is available in early access on [Google Play](https://play.google.com/store/apps/details?id=gallery.memories) or [GitHub Releases](https://github.com/pulsejet/memories/releases?q=android).
For automatic uploads, you can use the official Nextcloud mobile apps. These are available for [Android](https://play.google.com/store/apps/details?id=com.nextcloud.client) ([F-Droid](https://f-droid.org/en/packages/com.nextcloud.client/)) and [iOS](https://apps.apple.com/us/app/nextcloud/id1125420102).

View File

@ -101,7 +101,15 @@ occ memories:places-setup
### Error: Incorrect string value
If you get this error, it is likely that your database is not using the `utf8mb4` character set. Since the reverse geocoding database contains characters in various languages, it is necessary to use `utf8mb4` to store them. To fix this, you need to convert your database to use `utf8mb4`.
If you get this error (or an `Incorrect datetime value` error), it is likely that your database is not using the `utf8mb4` character set. Since the reverse geocoding database contains characters in various languages, it is necessary to use `utf8mb4` to store them. To fix this, you need to convert your database to use `utf8mb4`.
You can also try changing `/etc/myt.cnf` in your MySQL/MariaDB server to use `utf8mb4` by default:
```ini
init_connect='SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci'
```
Restart your database server after making this change.
## Transcoding

5
go-vod/.gitignore vendored
View File

@ -20,3 +20,8 @@ go-vod
# Go workspace file
go.work
# Binary folder
ffmpeg/
# RPdb token folder
token.txt

View File

@ -0,0 +1,78 @@
## Dependencies
Für die Ausführung von *go-vod* benötigen wir die [Jellyfin-Version](https://github.com/jellyfin/jellyfin-ffmpeg/releases) von FFmpeg. Die von Arch standardmäßig zur Verfügung gestellte Binary von FFmpeg funktioniert for *VAAPI* **nicht**.
## Beispiele
In dieser Anwendung passieren keine speziellen oder komplexen Sachen! Im Grunde wird das Video nur herunterskaliert und dann in "Stücken" dem Client zurückgegeben. Mehr passiert nicht!
Ein Beispiel hierfür kann folgendermaßen aussehen.
```sh
ffmpeg/ffmpeg -loglevel warning \
# Zu welcher Sekunde des Videos die Segmente starten sollen
-ss 16.000000 -hwaccel \
# VAAPI spezifische sachen
vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i ffmpeg/sample.mp4 -copyts \
# Flags zum herunterskalieren
-fflags +genpts -vf \ "format=nv12|vaapi,hwupload,scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080" \
# Video und audio mappen
-map "0:v:0" "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
# Teile das Video in 4 Sekunden lange segmente
-f hls -hls_flags split_by_time -hls_time 4 -hls_segment_type mpegts \
# Und schreibe diese als Segmente weg
-force_key_frames "expr:gte(t,n_forced*2)" -hls_segment_filename /home/ubuntugui/videos/1080p-%06d.ts -
# Zum kopieren
ffmpeg/ffmpeg -loglevel warning \
-ss 16.000000 -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i ffmpeg/sample.mp4 -copyts \
-fflags +genpts -vf \ "format=nv12|vaapi,hwupload,scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080" \
-map "0:v:0" -b:v 5M "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
-f hls -hls_flags split_by_time -hls_time 4 -hls_segment_type mpegts \
-force_key_frames "expr:gte(t,n_forced*2)" -hls_segment_filename /media/ubuntugui/videos/1080p-%06d.ts -
# Um die performance zu testen (ohne Segemente)
ffmpeg/ffmpeg -loglevel info \
-ss 16.000000 -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i ffmpeg/sample.mp4 -copyts \
-fflags +genpts -vf \ "format=nv12|vaapi,hwupload,scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080" \
-map "0:v:0" "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
/media/ubuntugui/videos/1080p.ts -y
# Optimierte Variante
ffmpeg/ffmpeg -loglevel info \
-ss 16.000000 -init_hw_device vaapi=dr:/dev/dri/renderD128 -hwaccel vaapi -hwaccel_device dr -hwaccel_output_format vaapi -filter_hw_device dr \
-i ffmpeg/sample.mp4 -copyts \
-fflags +genpts -vf 'scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080' \
-rc_mode VBR -qmax 30 -g 30 -b:v 5000k \
-map "0:v:0" -b:v 5M "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
/media/ubuntugui/videos/1080p.ts -y
# Quick Sync (Intel)
/media/ubuntugui/videos/ffmpeg -loglevel info \
-ss 16.000000 -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i /media/ubuntugui/videos/PXL_20231230_122849503.mp4 -copyts \
-fflags +genpts -vf "format=nv12|vaapi,hwupload,scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080" \
-map "0:v:0" -b:v 5M "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
/media/ubuntugui/videos/1080p.ts -y
# Vulkan Tests (haben nicht so ganz funktoniert....) -> nur nützlich, wenn man Farbkonvertierungen macht (vaapi -> vulkan -> konvertierungen -> vaapi)
ffmpeg/ffmpeg -loglevel info \
-ss 16.000000 -init_hw_device drm=dr:/dev/dri/renderD128 -init_hw_device vulkan@dr -hwaccel vaapi -filter_hw_device dr -hwaccel_output_format vaapi \
-i ffmpeg/sample.mp4 \
-vf "hwupload=derive_device=vulkan,scale_vulkan=w=1920:h=1080,hwmap=derive_device=vaapi,hwupload_vaapi" -autoscale 0 \
"-c:v" h264_vaapi -b:v 5M "-c:a" aac -ac 1 \
/media/ubuntugui/videos/1080p.ts -y
```
### Ergebnis
Eine Intel IGPU ist ungefähr *1.8x* so schnell wie eine dedizierte AMD Grafikkarte. Warum ist dies der Fall?
## Ausführen
Folgende Befehle für das Ausführen / Bauen der Binary.
```sh
go run ./
```

View File

@ -1,5 +1,14 @@
FROM linuxserver/ffmpeg:latest
FROM jellyfin/jellyfin:latest as base
RUN rm -rf /jellyfin && \
ln -s /usr/lib/jellyfin-ffmpeg/ffmpeg /usr/local/bin/ffmpeg && \
ln -s /usr/lib/jellyfin-ffmpeg/ffprobe /usr/local/bin/ffprobe
FROM scratch
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=base / /
COPY run.sh /go-vod.sh
EXPOSE 47788

View File

@ -1,10 +1,6 @@
# go-vod
Extremely minimal on-demand video transcoding server in go. Used by the FOSS photos app, [Memories](https://github.com/pulsejet/memories).
## Filing Issues
Please file issues at the [Memories](https://github.com/pulsejet/memories) repository.
Extremely minimal on-demand video transcoding server in go.
## Usage

View File

@ -1,3 +1,8 @@
module github.com/pulsejet/go-vod
module github.com/pulsejet/memories/go-vod
go 1.16
require (
git.rpjosh.de/RPJosh/RPdb/v4 v4.3.0 // indirect
git.rpjosh.de/RPJosh/go-logger v1.3.2
)

46
go-vod/go.sum 100644
View File

@ -0,0 +1,46 @@
git.rpjosh.de/RPJosh/RPdb/v4 v4.3.0 h1:7jh6MTOvDhieb5NGm6NMwAIz0aZWwmZiw0EghDwkCz0=
git.rpjosh.de/RPJosh/RPdb/v4 v4.3.0/go.mod h1:y++epwPmZfn+IG3dYYhFE6gXoTJRqLoiwV4QOIEv4XA=
git.rpjosh.de/RPJosh/go-logger v1.2.0/go.mod h1:iD3KaRyOIkYMj7E+xFMn5uDVCzW1lSJQopz1Fl1+BSM=
git.rpjosh.de/RPJosh/go-logger v1.3.2 h1:y8qFEBYeJzLLi6H7CpHHGb2pB0IyfHSG6m6o8TxL1uo=
git.rpjosh.de/RPJosh/go-logger v1.3.2/go.mod h1:iD3KaRyOIkYMj7E+xFMn5uDVCzW1lSJQopz1Fl1+BSM=
github.com/lesismal/llib v1.1.10/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg=
github.com/lesismal/nbio v1.3.10/go.mod h1:cBAu/+XwOfgzhuvl0KA953ZgLx9SxBZPLrp2mMX+Yxk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210513122933-cd7d49e622d5/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -5,10 +5,11 @@ import (
"log"
"os"
"github.com/pulsejet/go-vod/transcoder"
"git.rpjosh.de/RPJosh/go-logger"
"github.com/pulsejet/memories/go-vod/transcoder"
)
const VERSION = "0.1.28"
const VERSION = "0.2.4"
func main() {
// Build initial configuration
@ -16,10 +17,10 @@ func main() {
VersionMonitor: false,
Version: VERSION,
Bind: ":47788",
ChunkSize: 3,
LookBehind: 3,
ChunkSize: 2,
LookBehind: 4,
GoalBufferMin: 1,
GoalBufferMax: 4,
GoalBufferMax: 5,
StreamIdleTime: 60,
ManagerIdleTime: 60,
}
@ -36,7 +37,18 @@ func main() {
}
}
// Auto detect ffmpeg and ffprobe
// Configure logger
configureLogger()
// Initialize RPdb API
c.RPdbAPI = transcoder.GetRPdbAPI()
// Set ffmpeg path
c.FFmpeg = "./ffmpeg/ffmpeg"
c.FFprobe = "./ffmpeg/ffprobe"
c.VAAPI = true
// Auto-detect ffmpeg and ffprobe
c.AutoDetect()
// Start server
@ -46,3 +58,14 @@ func main() {
log.Println("Exiting go-vod with status code", code)
os.Exit(code)
}
// configureLogger configures the gloal logger with some default values
func configureLogger() {
globalLogger := logger.GetLoggerFromEnv(&logger.Logger{
File: &logger.FileLogger{},
Level: logger.LevelDebug,
ColoredOutput: true,
PrintSource: true,
})
logger.SetGlobalLogger(globalLogger)
}

Some files were not shown because too many files have changed in this diff Show More