├── .github
└── workflows
│ └── go_test.yml
├── .gitignore
├── .metadata
├── LICENSE
├── Makefile
├── README.md
├── README_EN.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ ├── libs
│ │ └── server-sources.jar
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── img_syncer
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── fonts
│ ├── Ubuntu-C.ttf
│ ├── Ubuntu-M.ttf
│ └── Ubuntu-Th.ttf
├── html
│ └── login_success
│ │ ├── index.go
│ │ └── index.html
├── icon
│ ├── pho_icon.png
│ └── pho_icon_white.png
├── images
│ ├── broken.png
│ └── gray.jpg
├── pay_qr
│ ├── alipay_qr.jpg
│ └── wechat_qr.png
├── pho-qq-group.jpg
└── screenshot
│ ├── Screenshot_01.png
│ ├── Screenshot_02.png
│ ├── Screenshot_03.png
│ ├── Screenshot_04.png
│ ├── Screenshot_05.png
│ ├── Screenshot_06.png
│ └── Screenshots.png
├── go.mod
├── go.sum
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Podfile
├── Podfile.lock
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
├── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ └── pho_icon_white.png
│ │ ├── Contents.json
│ │ └── LaunchImage.imageset
│ │ │ ├── Contents.json
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ └── README.md
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ └── Runner-Bridging-Header.h
└── RunnerTests
│ └── RunnerTests.swift
├── lib
├── asset.dart
├── background_sync_route.dart
├── choose_album_route.dart
├── event_bus.dart
├── gallery_body.dart
├── gallery_viewer_route.dart
├── global.dart
├── l10n
│ ├── app_en.arb
│ └── app_zh.arb
├── logger.dart
├── main.dart
├── proto
│ ├── img_syncer.pb.dart
│ ├── img_syncer.pbenum.dart
│ ├── img_syncer.pbgrpc.dart
│ └── img_syncer.pbjson.dart
├── run_server.dart
├── setting_body.dart
├── setting_storage_route.dart
├── state_model.dart
├── storage
│ └── storage.dart
├── storageform
│ ├── baidu_netdisk.dart
│ ├── nfsform.dart
│ ├── smbform.dart
│ └── webdavform.dart
├── sync_body.dart
├── sync_timer.dart
├── theme.dart
├── util.dart
└── video_route.dart
├── linux
├── .gitignore
├── CMakeLists.txt
├── flutter
│ ├── CMakeLists.txt
│ ├── generated_plugin_registrant.cc
│ ├── generated_plugin_registrant.h
│ └── generated_plugins.cmake
├── main.cc
├── my_application.cc
└── my_application.h
├── macos
├── .gitignore
├── Flutter
│ ├── Flutter-Debug.xcconfig
│ ├── Flutter-Release.xcconfig
│ └── GeneratedPluginRegistrant.swift
├── Podfile
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── app_icon_1024.png
│ │ ├── app_icon_128.png
│ │ ├── app_icon_16.png
│ │ ├── app_icon_256.png
│ │ ├── app_icon_32.png
│ │ ├── app_icon_512.png
│ │ └── app_icon_64.png
│ ├── Base.lproj
│ └── MainMenu.xib
│ ├── Configs
│ ├── AppInfo.xcconfig
│ ├── Debug.xcconfig
│ ├── Release.xcconfig
│ └── Warnings.xcconfig
│ ├── DebugProfile.entitlements
│ ├── Info.plist
│ ├── MainFlutterWindow.swift
│ └── Release.entitlements
├── proto
├── img_syncer.pb.go
├── img_syncer.proto
└── img_syncer_grpc.pb.go
├── pubspec.lock
├── pubspec.yaml
├── server
├── Dockerfile
├── api
│ ├── baidu.go
│ ├── http.go
│ ├── img.go
│ ├── img_test.go
│ ├── nfs.go
│ ├── nfs_test.go
│ ├── smb.go
│ ├── smb_test.go
│ ├── util_test.go
│ ├── webdav.go
│ └── webdav_test.go
├── drive
│ ├── baidu
│ │ ├── baidu.go
│ │ ├── errorno.go
│ │ ├── fs.go
│ │ └── token.go
│ ├── drive_test.go
│ ├── nfs
│ │ └── nfs.go
│ ├── smb
│ │ └── smb.go
│ └── webdav
│ │ └── webdav.go
├── imgmanager
│ ├── imgmanager.go
│ ├── interface.go
│ └── metadata.go
├── log.go
├── main.go
└── run
│ └── run.go
├── test
├── docker-compose.yml
├── nfs
│ └── exports
└── static
│ ├── static.go
│ └── test_pic_01.jpg
├── web
├── favicon.png
├── icons
│ ├── Icon-192.png
│ ├── Icon-512.png
│ ├── Icon-maskable-192.png
│ └── Icon-maskable-512.png
├── index.html
└── manifest.json
└── windows
├── .gitignore
├── CMakeLists.txt
├── flutter
├── CMakeLists.txt
├── generated_plugin_registrant.cc
├── generated_plugin_registrant.h
└── generated_plugins.cmake
└── runner
├── CMakeLists.txt
├── Runner.rc
├── flutter_window.cpp
├── flutter_window.h
├── main.cpp
├── resource.h
├── resources
└── app_icon.ico
├── runner.exe.manifest
├── utils.cpp
├── utils.h
├── win32_window.cpp
└── win32_window.h
/.github/workflows/go_test.yml:
--------------------------------------------------------------------------------
1 | name: golang test
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | go-test:
11 | runs-on: self-hosted
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-go@v3
15 | with:
16 | go-version: '1.18'
17 |
18 | - name: Build go binary
19 | run: make server
20 |
21 | - name: Run tests
22 | run: make test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .packages
31 | .pub-cache/
32 | .pub/
33 | /build/
34 |
35 | # Symbolication related
36 | app.*.symbols
37 |
38 | # Obfuscation related
39 | app.*.map.json
40 |
41 | # Android Studio will place build artifacts here
42 | /android/app/debug
43 | /android/app/profile
44 | /android/app/release
45 | output
46 | *.apk
47 | baidu_drive_test.go
48 | debug-info
49 | ios/Frameworks/RUN.xcframework
50 | android/app/libs/server.aar
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled.
5 |
6 | version:
7 | revision: 796c8ef79279f9c774545b3771238c3098dbefab
8 | channel: stable
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
17 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
18 | - platform: ios
19 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
20 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
21 |
22 | # User provided section
23 |
24 | # List of Local paths (relative to this file) that should be
25 | # ignored by the migrate tool.
26 | #
27 | # Files that are not part of the templates will be ignored by default.
28 | unmanaged_files:
29 | - 'lib/main.dart'
30 | - 'ios/Runner.xcodeproj/project.pbxproj'
31 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BUILD_VERSION := $(shell git describe --tags)
2 | GIT_COMMIT_SHA1 := $(shell git rev-parse HEAD)
3 | BUILD_TIME := $(shell date "+%F %T")
4 | BUILD_NAME := img_syncer_server
5 | VERSION_PACKAGE_NAME := github.com/fregie/PrintVersion
6 |
7 | DESCRIBE := img_syncer grpc server
8 |
9 | prebuild:
10 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1
11 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1.0
12 |
13 | protobuf:
14 | protoc -I. --go_out . --go_opt paths=source_relative \
15 | --go-grpc_out . --go-grpc_opt paths=source_relative \
16 | --dart_out=grpc:lib \
17 | proto/*.proto
18 |
19 | .PHONY: server
20 | server:
21 | CGO_ENABLED=0 go build -ldflags "\
22 | -X '${VERSION_PACKAGE_NAME}.Version=${BUILD_VERSION}' \
23 | -X '${VERSION_PACKAGE_NAME}.BuildTime=${BUILD_TIME}' \
24 | -X '${VERSION_PACKAGE_NAME}.GitCommitSHA1=${GIT_COMMIT_SHA1}' \
25 | -X '${VERSION_PACKAGE_NAME}.Describe=${DESCRIBE}' \
26 | -X '${VERSION_PACKAGE_NAME}.Name=${BUILD_NAME}'" \
27 | -o server/output/${BUILD_NAME} ./server
28 |
29 | server-aar: protobuf
30 | CGO_ENABLED=0 gomobile bind -target=android -androidapi 21 -ldflags "-s -w" -o android/app/libs/server.aar ./server/run
31 |
32 | server-ios: protobuf
33 | CGO_ENABLED=0 gomobile bind -target=ios -ldflags "-s -w" -o ios/Frameworks/RUN.xcframework ./server/run
34 |
35 | apk:
36 | flutter build apk --release --obfuscate --split-debug-info=./debug-info
37 |
38 | ipa:
39 | flutter build ipa --no-tree-shake-icons --obfuscate --split-debug-info=./debug-info
40 |
41 | .PHONY: test
42 | test:
43 | docker-compose -f test/docker-compose.yml up -d --build
44 | sleep 3
45 | go test -v ./server/api -p 1 -failfast
46 | go test -v ./server/drive -p 1 -failfast
47 | docker-compose -f test/docker-compose.yml down
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Pho - 一个用于查看和上传照片的无服务端应用
6 |
7 |
8 |
9 |
10 |
11 | 中文 | English
12 |
13 |
14 | ### 安装
15 | [下载apk](https://github.com/fregie/pho/releases)
16 |
17 | [Google Play](https://play.google.com/store/apps/details?id=com.fregie.pho)
18 |
19 | [App store](https://apps.apple.com/cn/app/pho-%E5%90%8C%E6%AD%A5%E7%85%A7%E7%89%87%E5%88%B0nas-%E7%BD%91%E7%9B%98/id6451428709)
20 |
21 | > 注: Google Play和App store的版本包含专业版功能,专业版功能未开源.
22 |
23 | ### 介绍
24 | 该应用的目的是替代手机上的自带相册应用,并且能够将照片同步到网络储存.
25 | 功能简单,只是用于查看照片以及同步照片到网络储存.试图做到优秀的体验.
26 |
27 | ### 功能
28 | * 本地照片查看
29 | * 云端照片查看
30 | * 增量同步照片到云端
31 | * 后台定期同步
32 | * 无数据库,无服务端
33 | * 以时间组织云端存储的目录结构
34 |
35 | ### 支持的网络储存
36 | - [x] Samba
37 | - [x] Webdav
38 | - [x] NFS
39 | - [x] 百度网盘
40 | - [ ] 阿里网盘
41 | - [ ] oneDrive
42 | - [ ] google drive
43 | - [ ] google photo
44 |
45 | ### Screenshots
46 |
47 |
48 |
49 |
50 | ### roadmap
51 | - [x] 支持放大/缩小图片
52 | - [x] 支持上传/浏览视频
53 | - [x] 支持NFS
54 | - [x] 支持百度网盘
55 | - [x] 支持IOS端
56 | - [ ] 支持desktop端
57 | - [x] 支持中文
58 |
59 | ### Contribute
60 | 感谢各位的积极反馈
61 |
62 | 给本项目提需求的还不少,但是我一个人精力有限,如果你有兴趣,欢迎加入.
63 |
64 | 可以在issue中回复沟通,帮忙一起做一些功能,提出你的pull request.
65 |
66 | ### 文件储存逻辑
67 | 本着尽可能简单的逻辑来储存文件,以时间为目录结构,以文件名为文件名储存源文件.在根目录创建一个`.thumbnail`目录来储存生成的缩略图,缩略图的目录结构与源文件相同.
68 | 你可以随时以其他形式利用你备份上去的照片,而不用依赖此app.
69 | 目录结构示意图:
70 | ```bash
71 | ├── 2022
72 | │ ├── 07
73 | │ │ ├── 02
74 | │ │ │ ├── 20220702_100940.JPG
75 | │ │ │ ├── 20220702_111416.JPG
76 | │ │ │ └── 20220702_111508.JPG
77 | │ │ └── 03
78 | │ │ ├── 20220703_101923.DNG
79 | │ │ ├── 20220703_112336.DNG
80 | │ │ └── 20220703_112338.DNG
81 | ├── 2023
82 | │ └── 01
83 | │ └── 03
84 | │ ├── 20230103_112348.JPG
85 | │ ├── 20230103_124634.JPG
86 | │ └── 20230103_124918.DNG
87 | └── .thumbnail
88 | └── 2022
89 | └── 07
90 | ├── 02
91 | │ ├── 20220702_100940.JPG
92 | │ ├── 20220702_111416.JPG
93 | │ └── 20220702_111508.JPG
94 | └── 03
95 | ├── 20220703_101923.DNG
96 | ├── 20220703_112336.DNG
97 | └── 20220703_112338.DNG
98 | ```
99 |
100 |
101 | ### Star History
102 |
103 | [](https://star-history.com/#fregie/pho&Date)
104 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Pho - A serverless application for viewing and uploading photos
6 |
7 |
8 |
9 |
10 |
11 | 中文 | English
12 |
13 |
14 | ### Installation
15 |
16 | [Download APK](https://github.com/fregie/pho/releases)
17 |
18 | [Google Play](https://play.google.com/store/apps/details?id=com.fregie.pho)
19 |
20 | [App store](https://apps.apple.com/cn/app/pho-%E5%90%8C%E6%AD%A5%E7%85%A7%E7%89%87%E5%88%B0nas-%E7%BD%91%E7%9B%98/id6451428709)
21 |
22 |
23 | ### Introduction
24 | The primary objective of this application is to serve as a replacement for the native photo gallery application on smartphones. It also offers the capability to synchronize photos with online storage.
25 | Pho is a simple app designed for viewing and synchronizing photos to cloud storage. It aims to provide an excellent user experience.
26 |
27 | ### Features
28 | * Local photo browsing
29 | * Cloud photo browsing
30 | * Incremental photo synchronization to the cloud
31 | * Background periodic synchronization
32 | * No database, no server-side
33 | * Organizing cloud storage directory structure by date
34 |
35 | ### Supported Cloud Storage
36 | - [x] Samba
37 | - [x] Webdav
38 | - [x] NFS
39 | - [ ] Alibaba Cloud Drive
40 | - [ ] baidu netdisk
41 | - [ ] oneDrive
42 | - [ ] google drive
43 | - [ ] google photo
44 |
45 | ### Screenshots
46 |
47 |
48 |
49 |
50 | ### Roadmap
51 | - [x] Support zooming in/out of images
52 | - [x] Support uploading/browsing videos
53 | - [x] Support NFS
54 | - [x] Support Baidu net disk
55 | - [x] Support iOS
56 | - [ ] Support web version
57 | - [x] Add Chinese
58 |
59 | ### Contribute
60 | Thank you all for your positive feedback.
61 |
62 | There have been quite a few people who have provided requirements for this project, but as an individual, my resources are limited. If you are interested, you are welcome to join.
63 |
64 | You can communicate by replying in the issue section and help in developing some features by submitting your pull request.
65 |
66 | ### File Storage Logic
67 | The application stores files based on a straightforward principle of utilizing the time as the directory structure, and the source file name as the filename for storage. A .thumbnail directory is created in the root directory to store the generated thumbnails, and the directory structure for these thumbnails aligns with that of the source files.
68 |
69 | You can access and utilize your backed-up photos in any other manner at any time, without dependence on this application.
70 |
71 | Directory Structure Diagram:
72 | ```bash
73 | ├── 2022
74 | │ ├── 07
75 | │ │ ├── 02
76 | │ │ │ ├── 20220702_100940.JPG
77 | │ │ │ ├── 20220702_111416.JPG
78 | │ │ │ └── 20220702_111508.JPG
79 | │ │ └── 03
80 | │ │ ├── 20220703_101923.DNG
81 | │ │ ├── 20220703_112336.DNG
82 | │ │ └── 20220703_112338.DNG
83 | ├── 2023
84 | │ └── 01
85 | │ └── 03
86 | │ ├── 20230103_112348.JPG
87 | │ ├── 20230103_124634.JPG
88 | │ └── 20230103_124918.DNG
89 | └── .thumbnail
90 | └── 2022
91 | └── 07
92 | ├── 02
93 | │ ├── 20220702_100940.JPG
94 | │ ├── 20220702_111416.JPG
95 | │ └── 20220702_111508.JPG
96 | └── 03
97 | ├── 20220703_101923.DNG
98 | ├── 20220703_112336.DNG
99 | └── 20220703_112338.DNG
100 | ```
101 |
102 | ### Star History
103 |
104 | [](https://star-history.com/#fregie/pho&Date)
105 |
106 | ### Join QQ group
107 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | compileSdkVersion flutter.compileSdkVersion
30 | ndkVersion flutter.ndkVersion
31 |
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_1_8
34 | targetCompatibility JavaVersion.VERSION_1_8
35 | }
36 |
37 | kotlinOptions {
38 | jvmTarget = '1.8'
39 | }
40 |
41 | sourceSets {
42 | main.java.srcDirs += 'src/main/kotlin'
43 | }
44 |
45 | defaultConfig {
46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
47 | applicationId "com.example.img_syncer"
48 | // You can update the following values to match your application needs.
49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
50 | minSdkVersion 21
51 | targetSdkVersion flutter.targetSdkVersion
52 | versionCode flutterVersionCode.toInteger()
53 | versionName flutterVersionName
54 | multiDexEnabled true
55 | }
56 |
57 | buildTypes {
58 | release {
59 | // TODO: Add your own signing config for the release build.
60 | // Signing with the debug keys for now, so `flutter run --release` works.
61 | signingConfig signingConfigs.debug
62 | applicationVariants.all { variant ->
63 | variant.outputs.all {
64 | outputFileName = "pho-${variant.name}-${variant.versionCode}-${variant.versionName}.apk"
65 | }
66 | }
67 | }
68 | debug {
69 | applicationVariants.all { variant ->
70 | variant.outputs.all {
71 | outputFileName = "pho-${variant.name}-${variant.versionCode}-${variant.versionName}.apk"
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
78 | flutter {
79 | source '../..'
80 | }
81 |
82 | dependencies {
83 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
84 | implementation fileTree(dir: 'libs', include: ['*.jar'])
85 | implementation(name: 'server', ext: 'aar') // 添加这一行,引入.aar文件
86 | implementation 'androidx.core:core-ktx:1.7.0'
87 | implementation 'androidx.appcompat:appcompat:1.4.1'
88 | implementation 'com.google.android.material:material:1.5.0'
89 | implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
90 | testImplementation 'junit:junit:4.13.2'
91 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
92 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
93 | }
94 |
--------------------------------------------------------------------------------
/android/app/libs/server-sources.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/libs/server-sources.jar
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
15 |
16 |
24 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
39 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/img_syncer/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.img_syncer
2 |
3 | import androidx.annotation.NonNull
4 | import io.flutter.embedding.android.FlutterActivity
5 | import io.flutter.embedding.engine.FlutterEngine
6 | import io.flutter.plugin.common.MethodChannel
7 | import run.Run
8 |
9 | import android.content.ContentUris
10 | import android.content.ContentValues
11 | import android.net.Uri
12 | import android.os.Build
13 | import android.provider.MediaStore
14 |
15 | import android.content.Intent
16 | import java.io.File
17 |
18 |
19 | class MainActivity : FlutterActivity() {
20 | private val CHANNEL = "com.example.img_syncer/RunGrpcServer"
21 |
22 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
23 | super.configureFlutterEngine(flutterEngine)
24 | MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
25 | call,
26 | result ->
27 | if (call.method == "RunGrpcServer") {
28 | val re = Run.runGrpcServer()
29 | result.success(re)
30 | } else if (call.method == "scanFile") {
31 | scanFile(call.argument("path"), call.argument("volumeName"), call.argument("relativePath"), call.argument("mimeType"))
32 | result.success(null)
33 | } else {
34 | result.notImplemented()
35 | }
36 | }
37 | }
38 |
39 | private fun scanFile(path: String?, volumeName: String?, relativePath: String?, mimeType: String?) {
40 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
41 | // val values = ContentValues().apply {
42 | // put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
43 | // put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
44 | // put(MediaStore.MediaColumns.IS_PENDING, 1)
45 | // }
46 |
47 | // val contentUri: Uri = MediaStore.Files.getContentUri(volumeName)
48 | // val itemUri = contentResolver.insert(contentUri, values)
49 |
50 | // values.clear()
51 | // values.put(MediaStore.MediaColumns.IS_PENDING, 0)
52 | // contentResolver.update(itemUri!!, values, null, null)
53 | // } else {
54 | val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
55 | val file = File(path)
56 | val contentUri = Uri.fromFile(file)
57 | mediaScanIntent.data = contentUri
58 | sendBroadcast(mediaScanIntent)
59 | // }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-hdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-mdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.8.0'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:7.2.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | flatDir {
19 | dirs 'libs' // 添加这一行,表示在libs目录下寻找.aar文件
20 | }
21 | }
22 | }
23 |
24 | rootProject.buildDir = '../build'
25 | subprojects {
26 | project.buildDir = "${rootProject.buildDir}/${project.name}"
27 | }
28 | subprojects {
29 | project.evaluationDependsOn(':app')
30 | }
31 |
32 | tasks.register("clean", Delete) {
33 | delete rootProject.buildDir
34 | }
35 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
6 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | def properties = new Properties()
5 |
6 | assert localPropertiesFile.exists()
7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 |
9 | def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 |
--------------------------------------------------------------------------------
/assets/fonts/Ubuntu-C.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/fonts/Ubuntu-C.ttf
--------------------------------------------------------------------------------
/assets/fonts/Ubuntu-M.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/fonts/Ubuntu-M.ttf
--------------------------------------------------------------------------------
/assets/fonts/Ubuntu-Th.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/fonts/Ubuntu-Th.ttf
--------------------------------------------------------------------------------
/assets/html/login_success/index.go:
--------------------------------------------------------------------------------
1 | package login_success_html
2 |
3 | import (
4 | _ "embed"
5 | )
6 |
7 | //go:embed index.html
8 | var Html []byte
9 |
--------------------------------------------------------------------------------
/assets/html/login_success/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 登录成功
8 |
9 |
10 |
44 |
45 |
46 |
47 |
48 |
check_circle
49 |
登录成功
50 |
你现在可以关闭此页面并返回pho了
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/assets/icon/pho_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/icon/pho_icon.png
--------------------------------------------------------------------------------
/assets/icon/pho_icon_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/icon/pho_icon_white.png
--------------------------------------------------------------------------------
/assets/images/broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/images/broken.png
--------------------------------------------------------------------------------
/assets/images/gray.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/images/gray.jpg
--------------------------------------------------------------------------------
/assets/pay_qr/alipay_qr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/pay_qr/alipay_qr.jpg
--------------------------------------------------------------------------------
/assets/pay_qr/wechat_qr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/pay_qr/wechat_qr.png
--------------------------------------------------------------------------------
/assets/pho-qq-group.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/pho-qq-group.jpg
--------------------------------------------------------------------------------
/assets/screenshot/Screenshot_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/screenshot/Screenshot_01.png
--------------------------------------------------------------------------------
/assets/screenshot/Screenshot_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/screenshot/Screenshot_02.png
--------------------------------------------------------------------------------
/assets/screenshot/Screenshot_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/screenshot/Screenshot_03.png
--------------------------------------------------------------------------------
/assets/screenshot/Screenshot_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/screenshot/Screenshot_04.png
--------------------------------------------------------------------------------
/assets/screenshot/Screenshot_05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/screenshot/Screenshot_05.png
--------------------------------------------------------------------------------
/assets/screenshot/Screenshot_06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/screenshot/Screenshot_06.png
--------------------------------------------------------------------------------
/assets/screenshot/Screenshots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/assets/screenshot/Screenshots.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/fregie/img_syncer
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/fregie/PrintVersion v0.1.0
7 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c
8 | google.golang.org/grpc v1.53.0
9 | google.golang.org/protobuf v1.28.1
10 | )
11 |
12 | require (
13 | github.com/davecgh/go-spew v1.1.1 // indirect
14 | github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
15 | github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
16 | github.com/geoffgarside/ber v1.1.0 // indirect
17 | github.com/go-errors/errors v1.4.2 // indirect
18 | github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
19 | github.com/pmezard/go-difflib v1.0.0 // indirect
20 | github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect
21 | golang.org/x/crypto v0.7.0 // indirect
22 | golang.org/x/mod v0.8.0 // indirect
23 | golang.org/x/tools v0.6.0 // indirect
24 | gopkg.in/yaml.v2 v2.4.0 // indirect
25 | gopkg.in/yaml.v3 v3.0.1 // indirect
26 | )
27 |
28 | require (
29 | github.com/Workiva/go-datastructures v1.0.53
30 | github.com/dsoprea/go-exif/v3 v3.0.0-20221012082141-d21ac8e2de85
31 | github.com/golang/protobuf v1.5.2 // indirect
32 | github.com/hirochachacha/go-smb2 v1.1.0
33 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
34 | github.com/stretchr/testify v1.8.2
35 | github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2
36 | github.com/vmware/go-nfs-client v0.0.0-20190605212624-d43b92724c1b
37 | golang.org/x/net v0.9.0
38 | golang.org/x/sys v0.7.0 // indirect
39 | golang.org/x/text v0.9.0 // indirect
40 | google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
41 | )
42 |
43 | // replace github.com/studio-b12/gowebdav => ../gowebdav
44 | replace github.com/vmware/go-nfs-client => github.com/fregie/go-nfs-client v1.0.0
45 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | **/dgph
2 | *.mode1v3
3 | *.mode2v3
4 | *.moved-aside
5 | *.pbxuser
6 | *.perspectivev3
7 | **/*sync/
8 | .sconsign.dblite
9 | .tags*
10 | **/.vagrant/
11 | **/DerivedData/
12 | Icon?
13 | **/Pods/
14 | **/.symlinks/
15 | profile
16 | xcuserdata
17 | **/.generated/
18 | Flutter/App.framework
19 | Flutter/Flutter.framework
20 | Flutter/Flutter.podspec
21 | Flutter/Generated.xcconfig
22 | Flutter/ephemeral/
23 | Flutter/app.flx
24 | Flutter/app.zip
25 | Flutter/flutter_assets/
26 | Flutter/flutter_export_environment.sh
27 | ServiceDefinitions.json
28 | Runner/GeneratedPluginRegistrant.*
29 |
30 | # Exceptions to above rules.
31 | !default.mode1v3
32 | !default.mode2v3
33 | !default.pbxuser
34 | !default.perspectivev3
35 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 11.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | platform :ios, '11.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35 | target 'RunnerTests' do
36 | inherit! :search_paths
37 | end
38 | end
39 |
40 | post_install do |installer|
41 | installer.pods_project.targets.each do |target|
42 | flutter_additional_ios_build_settings(target)
43 | target.build_configurations.each do |config|
44 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
45 | '$(inherited)',
46 | ## dart: PermissionGroup.calendar
47 | 'PERMISSION_EVENTS=0',
48 |
49 | ## dart: PermissionGroup.reminders
50 | 'PERMISSION_REMINDERS=0',
51 |
52 | ## dart: PermissionGroup.contacts
53 | 'PERMISSION_CONTACTS=0',
54 |
55 | ## dart: PermissionGroup.camera
56 | 'PERMISSION_CAMERA=0',
57 |
58 | ## dart: PermissionGroup.microphone
59 | 'PERMISSION_MICROPHONE=0',
60 |
61 | ## dart: PermissionGroup.speech
62 | 'PERMISSION_SPEECH_RECOGNIZER=0',
63 |
64 | ## dart: PermissionGroup.photos
65 | 'PERMISSION_PHOTOS=1',
66 |
67 | ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
68 | 'PERMISSION_LOCATION=0',
69 |
70 | ## dart: PermissionGroup.notification
71 | 'PERMISSION_NOTIFICATIONS=0',
72 |
73 | ## dart: PermissionGroup.mediaLibrary
74 | 'PERMISSION_MEDIA_LIBRARY=0',
75 |
76 | ## dart: PermissionGroup.sensors
77 | 'PERMISSION_SENSORS=0',
78 |
79 | ## dart: PermissionGroup.bluetooth
80 | 'PERMISSION_BLUETOOTH=0',
81 |
82 | ## dart: PermissionGroup.appTrackingTransparency
83 | 'PERMISSION_APP_TRACKING_TRANSPARENCY=0',
84 |
85 | ## dart: PermissionGroup.criticalAlerts
86 | 'PERMISSION_CRITICAL_ALERTS=0'
87 | ]
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 | import RUN
4 |
5 | @UIApplicationMain
6 | @objc class AppDelegate: FlutterAppDelegate {
7 | override func application(
8 | _ application: UIApplication,
9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
10 | ) -> Bool {
11 | let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
12 | let batteryChannel = FlutterMethodChannel(name: "com.example.img_syncer/RunGrpcServer",binaryMessenger: controller.binaryMessenger)
13 | batteryChannel.setMethodCallHandler({
14 | (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
15 | var error: NSError? = nil
16 | let ports = RunRunGrpcServer(&error)
17 | result(ports)
18 | })
19 |
20 | GeneratedPluginRegistrant.register(with: self)
21 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "pho_icon_white.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/pho_icon_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/ios/Runner/Assets.xcassets/AppIcon.appiconset/pho_icon_white.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleDisplayName
10 | Pho
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | pho
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | $(FLUTTER_BUILD_NAME)
23 | CFBundleSignature
24 | ????
25 | CFBundleVersion
26 | $(FLUTTER_BUILD_NUMBER)
27 | LSRequiresIPhoneOS
28 |
29 | NSPhotoLibraryAddUsageDescription
30 | Allow access to photos to save photos to your library.
31 | NSPhotoLibraryUsageDescription
32 | Allow access to photos to upload photos from your library.
33 | UIApplicationSupportsIndirectInputEvents
34 |
35 | UILaunchStoryboardName
36 | LaunchScreen
37 | UIMainStoryboardFile
38 | Main
39 | UISupportedInterfaceOrientations
40 |
41 | UIInterfaceOrientationPortrait
42 |
43 | UISupportedInterfaceOrientations~ipad
44 |
45 | UIInterfaceOrientationPortrait
46 | UIInterfaceOrientationPortraitUpsideDown
47 | UIInterfaceOrientationLandscapeLeft
48 | UIInterfaceOrientationLandscapeRight
49 |
50 | UIViewControllerBasedStatusBarAppearance
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/ios/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 | import XCTest
4 |
5 | class RunnerTests: XCTestCase {
6 |
7 | func testExample() {
8 | // If you add code to the Runner application, consider adding tests here.
9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/lib/background_sync_route.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:photo_manager/photo_manager.dart';
3 | import 'package:shared_preferences/shared_preferences.dart';
4 | import 'package:img_syncer/sync_timer.dart';
5 | import 'package:img_syncer/state_model.dart';
6 | import 'package:img_syncer/global.dart';
7 |
8 | class BackgroundSyncSettingRoute extends StatefulWidget {
9 | const BackgroundSyncSettingRoute({Key? key}) : super(key: key);
10 |
11 | @override
12 | _BackgroundSyncSettingRouteState createState() =>
13 | _BackgroundSyncSettingRouteState();
14 | }
15 |
16 | class _BackgroundSyncSettingRouteState
17 | extends State {
18 | bool _backgroundSyncEnabled = false;
19 | bool _backgroundSyncWifiOnly = true;
20 | Duration _backgroundSyncInterval = const Duration(minutes: 60);
21 | List albums = [];
22 |
23 | @override
24 | void initState() {
25 | super.initState();
26 | _loadSettings();
27 | }
28 |
29 | Future _loadSettings() async {
30 | final prefs = await SharedPreferences.getInstance();
31 | setState(() {
32 | _backgroundSyncEnabled = prefs.getBool('backgroundSyncEnabled') ?? false;
33 | _backgroundSyncWifiOnly = prefs.getBool('backgroundSyncWifiOnly') ?? true;
34 | _backgroundSyncInterval =
35 | Duration(minutes: prefs.getInt('backgroundSyncInterval') ?? 60);
36 | });
37 | final re = await requestPermission();
38 | if (!re) return;
39 | albums = await PhotoManager.getAssetPathList(type: RequestType.common);
40 | for (var path in albums) {
41 | if (path.name == 'Recent') {
42 | albums.remove(path);
43 | break;
44 | }
45 | }
46 | setState(() {});
47 | }
48 |
49 | @override
50 | Widget build(BuildContext context) {
51 | return Scaffold(
52 | backgroundColor: Theme.of(context).scaffoldBackgroundColor,
53 | appBar: AppBar(
54 | backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
55 | iconTheme: Theme.of(context).iconTheme,
56 | elevation: 0,
57 | title: Text(l10n.backgroundSync,
58 | style: Theme.of(context).textTheme.titleLarge),
59 | ),
60 | body: ListView(
61 | children: [
62 | ListTile(
63 | title: Text(l10n.enableBackgroundSync),
64 | trailing: Switch(
65 | value: _backgroundSyncEnabled,
66 | onChanged: (value) async {
67 | final prefs = await SharedPreferences.getInstance();
68 | await prefs.setBool('backgroundSyncEnabled', value);
69 | setState(() {
70 | _backgroundSyncEnabled = value;
71 | });
72 | reloadAutoSyncTimer();
73 | },
74 | ),
75 | ),
76 | ListTile(
77 | title: Text(l10n.syncOnlyOnWifi),
78 | trailing: Switch(
79 | value: _backgroundSyncWifiOnly,
80 | onChanged: (value) async {
81 | final prefs = await SharedPreferences.getInstance();
82 | await prefs.setBool('backgroundSyncWifiOnly', value);
83 | setState(() {
84 | _backgroundSyncWifiOnly = value;
85 | });
86 | reloadAutoSyncTimer();
87 | },
88 | ),
89 | ),
90 | ListTile(
91 | title: Text(l10n.syncInterval),
92 | trailing: DropdownButton(
93 | value: _backgroundSyncInterval,
94 | items: [
95 | // DropdownMenuItem(
96 | // value: Duration(minutes: 1),
97 | // child: Text('1 minute'),
98 | // ),
99 | DropdownMenuItem(
100 | value: const Duration(minutes: 10),
101 | child: Text('10 ${l10n.minite}'),
102 | ),
103 | DropdownMenuItem(
104 | value: const Duration(hours: 1),
105 | child: Text('1 ${l10n.hour}'),
106 | ),
107 | DropdownMenuItem(
108 | value: const Duration(hours: 3),
109 | child: Text('3 ${l10n.hour}'),
110 | ),
111 | DropdownMenuItem(
112 | value: const Duration(hours: 6),
113 | child: Text('6 ${l10n.hour}'),
114 | ),
115 | DropdownMenuItem(
116 | value: const Duration(hours: 12),
117 | child: Text('12 ${l10n.hour}'),
118 | ),
119 | DropdownMenuItem(
120 | value: const Duration(days: 1),
121 | child: Text('1 ${l10n.day}'),
122 | ),
123 | DropdownMenuItem(
124 | value: const Duration(days: 3),
125 | child: Text('3 ${l10n.day}'),
126 | ),
127 | DropdownMenuItem(
128 | value: const Duration(days: 7),
129 | child: Text('1 ${l10n.week}'),
130 | ),
131 | ],
132 | onChanged: (value) async {
133 | final prefs = await SharedPreferences.getInstance();
134 | await prefs.setInt('backgroundSyncInterval', value!.inMinutes);
135 | setState(() {
136 | _backgroundSyncInterval = value;
137 | });
138 | reloadAutoSyncTimer();
139 | },
140 | ),
141 | ),
142 | ],
143 | ),
144 | );
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/lib/event_bus.dart:
--------------------------------------------------------------------------------
1 | import 'package:event_bus/event_bus.dart';
2 |
3 | //Bus初始化
4 | EventBus eventBus = EventBus();
5 |
6 | class LocalRefreshEvent {
7 | LocalRefreshEvent();
8 | }
9 |
10 | class RemoteRefreshEvent {
11 | RemoteRefreshEvent();
12 | }
13 |
--------------------------------------------------------------------------------
/lib/l10n/app_en.arb:
--------------------------------------------------------------------------------
1 | {
2 | "local": "Local",
3 | "cloud": "Cloud",
4 | "sync": "Sync",
5 | "cloudSync": "Cloud sync",
6 | "localFolder": "Local folder",
7 | "cloudStorage": "Cloud storage",
8 | "backgroundSync": "Background sync",
9 | "notSync": "not sync",
10 | "unsynchronizedPhotos": "Unsynchronized photos",
11 | "date": "Date",
12 | "delete": "Delete",
13 | "photos": "photos",
14 | "deleteThisPhoto": "Delete this photo",
15 | "deleteThisPhotos": "Delete this photos",
16 | "cantBeUndone": "This action can't be undone",
17 | "download": "Download",
18 | "upload": "Upload",
19 | "success": "success",
20 | "pics": "pics",
21 | "choose": "Choose",
22 | "stop": "Stop",
23 | "uploading": "Uploading",
24 | "downloading": "Downloading",
25 | "uploadFailed": "Upload failed",
26 | "uploaded": "Uploaded",
27 | "notUploaded": "Not uploaded",
28 | "chooseAlbum": "Choose album",
29 | "storageSetting": "Storage setting",
30 | "remoteStorageType": "Remote storage type",
31 | "samvbaServerAddress": "Samba server address",
32 | "username": "Username",
33 | "password": "Password",
34 | "share": "Share",
35 | "rootPath": "Root path(Your photos will be uploaded to this path)",
36 | "optional": "optional",
37 | "testStorage": "Test storage",
38 | "save": "Save",
39 | "enableBackgroundSync": "Enable background sync",
40 | "syncOnlyOnWifi": "Sync only on WIFI",
41 | "syncInterval": "Sync interval",
42 | "minite": "minite",
43 | "hour": "hour",
44 | "day": "day",
45 | "week": "week",
46 | "chineseday": "",
47 | "yes": "Yes",
48 | "cancel": "Cancel",
49 | "permissionDenied": "Permission denied",
50 | "setLocalFirst": "Please set local folder first",
51 | "downloadFailed": "Download failed",
52 | "storageNotSetted": "Remote storage is not setted,please set it first",
53 | "successfullyUpload": "Successfully upload",
54 | "testSuccess": "Test success,you can save now",
55 | "connectFailed": "Storage connection failed",
56 | "selectRoot": "Select root path",
57 | "currentPath": "Current path",
58 | "baiduNetdisk": "Baidu Netdisk",
59 | "baiduNetdiskLogin": "Baidu Netdisk Login",
60 | "refreshingPleaseWait": "Comparing your local and cloud photos, if there are many photos, it may take some time. Please be patient......",
61 | "setRemoteStroage": "Please set cloud storage first",
62 | "needPermision": "Need permission to access photos",
63 | "gotoSystemSetting": "You can go to system settings to change the permission",
64 | "openSetting": "Open settings"
65 | }
--------------------------------------------------------------------------------
/lib/l10n/app_zh.arb:
--------------------------------------------------------------------------------
1 | {
2 | "local": "本地",
3 | "cloud": "云端",
4 | "sync": "同步",
5 | "cloudSync": "云端同步",
6 | "localFolder": "本地相册",
7 | "cloudStorage": "云端设置",
8 | "backgroundSync": "后台同步",
9 | "notSync": "张照片尚未同步",
10 | "unsynchronizedPhotos": "未同步照片",
11 | "date": "日期",
12 | "delete": "删除",
13 | "photos": "照片",
14 | "deleteThisPhoto": "删除这张照片",
15 | "deleteThisPhotos": "删除选中的照片",
16 | "cantBeUndone": "该操作无法撤销",
17 | "download": "下载",
18 | "upload": "上传",
19 | "success": "成功",
20 | "pics": "照片",
21 | "choose": "选择",
22 | "stop": "停止",
23 | "uploading": "上传中",
24 | "downloading": "下载中",
25 | "uploadFailed": "上传失败",
26 | "uploaded": "已上传",
27 | "notUploaded": "未上传",
28 | "chooseAlbum": "选择相册",
29 | "storageSetting": "网络储存设置",
30 | "remoteStorageType": "网络储存类型",
31 | "samvbaServerAddress": "Samba服务器地址",
32 | "username": "用户名",
33 | "password": "密码",
34 | "share": "分享",
35 | "rootPath": "储存根目录(照片会储存在该目录下)",
36 | "optional": "可选",
37 | "testStorage": "测试连接",
38 | "save": "保存",
39 | "enableBackgroundSync": "启用后台同步",
40 | "syncOnlyOnWifi": "仅在连接WIFI时同步",
41 | "syncInterval": "同步间隔",
42 | "minite": "分钟",
43 | "hour": "小时",
44 | "day": "天",
45 | "week": "周",
46 | "chineseday": "日",
47 | "yes": "确认",
48 | "cancel": "取消",
49 | "permissionDenied": "权限不足",
50 | "setLocalFirst": "请先设置本地相册",
51 | "downloadFailed": "下载失败",
52 | "storageNotSetted": "网络储存未配置,请先配置网络储存",
53 | "successfullyUpload": "成功上传",
54 | "testSuccess": "连接成功,请点击保存",
55 | "connectFailed": "连接失败",
56 | "selectRoot": "选择根目录",
57 | "currentPath": "当前目录",
58 | "baiduNetdisk": "百度网盘",
59 | "baiduNetdiskLogin": "百度网盘登录",
60 | "refreshingPleaseWait": "正在交叉对比你本地和云端的照片,如果照片数量较多可能耗时较久,请耐心等待......",
61 | "setRemoteStroage": "请先点击云端设置设置网络储存",
62 | "needPermision": "需要访问相册的权限",
63 | "gotoSystemSetting": "请转至系统设置授予相册的权限",
64 | "openSetting": "打开设置"
65 | }
--------------------------------------------------------------------------------
/lib/logger.dart:
--------------------------------------------------------------------------------
1 | import 'package:logger/logger.dart';
2 |
3 | final logger = Logger();
4 |
--------------------------------------------------------------------------------
/lib/proto/img_syncer.pbenum.dart:
--------------------------------------------------------------------------------
1 | ///
2 | // Generated code. Do not modify.
3 | // source: proto/img_syncer.proto
4 | //
5 | // @dart = 2.12
6 | // ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
7 |
8 |
--------------------------------------------------------------------------------
/lib/run_server.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/services.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | Future runServer() async {
7 | late String ports;
8 | WidgetsFlutterBinding.ensureInitialized();
9 |
10 | if (Platform.isAndroid) {
11 | ports = await const MethodChannel('com.example.img_syncer/RunGrpcServer')
12 | .invokeMethod('RunGrpcServer');
13 | } else if (Platform.isIOS) {
14 | ports = await const MethodChannel('com.example.img_syncer/RunGrpcServer')
15 | .invokeMethod('RunGrpcServer');
16 | }
17 |
18 | return ports;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/setting_body.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:img_syncer/choose_album_route.dart';
3 | import 'package:img_syncer/setting_storage_route.dart';
4 |
5 | class SettingBody extends StatelessWidget {
6 | const SettingBody({Key? key}) : super(key: key);
7 |
8 | @override
9 | Widget build(BuildContext context) {
10 | final ButtonStyle style = FilledButton.styleFrom(
11 | shape: RoundedRectangleBorder(
12 | borderRadius: BorderRadius.circular(10),
13 | ));
14 | return LayoutBuilder(
15 | builder: (context, constraints) {
16 | return Column(
17 | children: [
18 | Row(
19 | children: [
20 | Container(
21 | height: 60,
22 | width: constraints.maxWidth * 0.5,
23 | padding: const EdgeInsets.fromLTRB(10, 5, 5, 5),
24 | child: FilledButton.tonal(
25 | style: style,
26 | onPressed: () {
27 | Navigator.push(
28 | context,
29 | MaterialPageRoute(
30 | builder: (context) => const ChooseAlbumRoute()),
31 | );
32 | },
33 | child: const Row(
34 | children: [
35 | Icon(
36 | Icons.folder_outlined,
37 | // color: Theme.of(context).colorScheme.secondary,
38 | ),
39 | SizedBox(width: 10),
40 | Text('local folder'),
41 | ],
42 | ),
43 | ),
44 | ),
45 | Container(
46 | height: 60,
47 | width: constraints.maxWidth * 0.5,
48 | padding: const EdgeInsets.fromLTRB(5, 5, 10, 5),
49 | child: FilledButton.tonal(
50 | style: style,
51 | onPressed: () {
52 | Navigator.push(
53 | context,
54 | MaterialPageRoute(
55 | builder: (context) => const SettingStorageRoute(),
56 | ));
57 | },
58 | child: const Row(
59 | children: [
60 | Icon(
61 | Icons.cloud_outlined,
62 | // color: Theme.of(context).colorScheme.secondary,
63 | ),
64 | SizedBox(width: 10),
65 | Text('Cloud storage'),
66 | ],
67 | ),
68 | ),
69 | ),
70 | ],
71 | ),
72 | ],
73 | );
74 | },
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/lib/setting_storage_route.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:img_syncer/storageform/smbform.dart';
3 | import 'package:img_syncer/storageform/webdavform.dart';
4 | import 'package:img_syncer/storageform/nfsform.dart';
5 | import 'package:img_syncer/storageform/baidu_netdisk.dart';
6 | import 'package:shared_preferences/shared_preferences.dart';
7 | import 'package:img_syncer/state_model.dart';
8 | import 'package:img_syncer/global.dart';
9 |
10 | class SettingStorageRoute extends StatefulWidget {
11 | const SettingStorageRoute({Key? key}) : super(key: key);
12 |
13 | @override
14 | SettingStorageRouteState createState() => SettingStorageRouteState();
15 | }
16 |
17 | Drive getDrive(String drive) {
18 | return driveName.entries
19 | .firstWhere((element) => element.value == drive,
20 | orElse: () => const MapEntry(Drive.smb, "SMB"))
21 | .key;
22 | }
23 |
24 | class SettingStorageRouteState extends State {
25 | final GlobalKey _formKey = GlobalKey();
26 |
27 | @protected
28 | Drive currentDrive = Drive.smb;
29 |
30 | @override
31 | void initState() {
32 | super.initState();
33 | SharedPreferences.getInstance().then((prefs) {
34 | final drive = prefs.getString("drive");
35 | if (drive != null) {
36 | setState(() {
37 | currentDrive = getDrive(drive);
38 | });
39 | }
40 | });
41 | }
42 |
43 | @override
44 | Widget build(BuildContext context) {
45 | late Widget form;
46 | switch (currentDrive) {
47 | case Drive.smb:
48 | form = const SMBForm();
49 | break;
50 | case Drive.webDav:
51 | form = const WebDavForm();
52 | break;
53 | case Drive.nfs:
54 | form = const NFSForm();
55 | break;
56 | case Drive.baiduNetdisk:
57 | form = const BaiduNetdiskForm();
58 | break;
59 | default:
60 | form = const Text('Not implemented');
61 | }
62 | return Scaffold(
63 | appBar: AppBar(
64 | backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
65 | iconTheme: Theme.of(context).iconTheme,
66 | elevation: 0,
67 | title: Text(l10n.storageSetting,
68 | style: Theme.of(context).textTheme.titleLarge),
69 | ),
70 | body: Center(
71 | child: Column(
72 | children: [
73 | Container(
74 | padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
75 | child: TextField(
76 | readOnly: true,
77 | controller: TextEditingController(
78 | text: driveName[currentDrive] == "BaiduNetdisk"
79 | ? l10n.baiduNetdisk
80 | : driveName[currentDrive]),
81 | decoration: InputDecoration(
82 | border: const OutlineInputBorder(),
83 | labelText: l10n.remoteStorageType,
84 | suffixIcon: PopupMenuButton(
85 | icon: const Icon(Icons.arrow_drop_down),
86 | itemBuilder: (BuildContext context) {
87 | return driveName.values
88 | .map((String value) => PopupMenuItem(
89 | value: value,
90 | child: Text(value == "BaiduNetdisk"
91 | ? l10n.baiduNetdisk
92 | : value),
93 | ))
94 | .toList();
95 | },
96 | onSelected: (String value) => setState(() {
97 | currentDrive = getDrive(value);
98 | SharedPreferences.getInstance().then((prefs) {
99 | prefs.setString("drive", value);
100 | });
101 | }),
102 | )),
103 | ),
104 | ),
105 | form,
106 | ],
107 | )));
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/lib/storageform/baidu_netdisk.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:img_syncer/event_bus.dart';
5 | import 'package:img_syncer/proto/img_syncer.pbgrpc.dart';
6 | import 'package:img_syncer/state_model.dart';
7 | import 'package:img_syncer/storage/storage.dart';
8 | import 'package:shared_preferences/shared_preferences.dart';
9 | import 'package:img_syncer/global.dart';
10 | import 'package:url_launcher/url_launcher.dart';
11 | import 'package:path_provider/path_provider.dart';
12 |
13 | class BaiduNetdiskForm extends StatefulWidget {
14 | const BaiduNetdiskForm({Key? key}) : super(key: key);
15 | @override
16 | BaiduNetdiskFormState createState() => BaiduNetdiskFormState();
17 | }
18 |
19 | class BaiduNetdiskFormState extends State {
20 | bool loginSuccess = false;
21 | StartBaiduNetdiskLoginResponse? rsp;
22 |
23 | Widget saveButtun() {
24 | return Container(
25 | width: 150,
26 | padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
27 | child: FilledButton(
28 | onPressed: loginSuccess
29 | ? () {
30 | SharedPreferences.getInstance().then((prefs) {
31 | prefs.setString("baidu_refresh_token", rsp!.refreshToken);
32 | prefs.setString("baidu_access_token", rsp!.accessToken);
33 | prefs.setInt("baidu_expires_at", rsp!.exiresAt.toInt());
34 | prefs.setString("drive", driveName[Drive.baiduNetdisk]!);
35 | });
36 | settingModel.setRemoteStorageSetted(true);
37 | assetModel.remoteLastError = null;
38 | eventBus.fire(RemoteRefreshEvent());
39 | Navigator.pop(context);
40 | }
41 | : null,
42 | child: Text(
43 | l10n.save,
44 | textAlign: TextAlign.center,
45 | style: const TextStyle(height: 1.0),
46 | ),
47 | ),
48 | );
49 | }
50 |
51 | @override
52 | Widget build(BuildContext context) {
53 | return Center(
54 | child: Row(
55 | mainAxisAlignment: MainAxisAlignment.center,
56 | children: [
57 | FilledButton(
58 | child: Text(
59 | l10n.baiduNetdiskLogin,
60 | textAlign: TextAlign.center,
61 | style: const TextStyle(height: 1.0),
62 | ),
63 | onPressed: () async {
64 | final temporaryDir = await getTemporaryDirectory();
65 | storage.cli
66 | .startBaiduNetdiskLogin(StartBaiduNetdiskLoginRequest(
67 | tmpDir: temporaryDir.path,
68 | ))
69 | .then((StartBaiduNetdiskLoginResponse resp) {
70 | rsp = resp;
71 | if (rsp!.success) {
72 | SnackBarManager.showSnackBar(l10n.testSuccess);
73 | setState(() {
74 | loginSuccess = true;
75 | });
76 | }
77 | });
78 | final Uri authUrl = Uri.parse(
79 | 'http://openapi.baidu.com/oauth/2.0/authorize?response_type=code&client_id=8wylQfdIzIpNFOGHZSnOOQ98QLDFvl1U&redirect_uri=http://localhost.pho.tools:$httpPort/baidu/callback&scope=basic,netdisk&device_id=34906909&display=mobile');
80 | LaunchMode mode = LaunchMode.externalApplication;
81 | if (Platform.isIOS) {
82 | mode = LaunchMode.inAppWebView;
83 | }
84 | final success = await launchUrl(authUrl, mode: mode);
85 | if (!success) {
86 | throw Exception('Could not launch $authUrl');
87 | }
88 | },
89 | ),
90 | saveButtun(),
91 | ],
92 | ));
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/sync_timer.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:connectivity_plus/connectivity_plus.dart';
4 | import 'package:img_syncer/state_model.dart';
5 | import 'package:shared_preferences/shared_preferences.dart';
6 | import 'package:photo_manager/photo_manager.dart';
7 | import 'package:img_syncer/storage/storage.dart';
8 | import 'package:img_syncer/event_bus.dart';
9 | import 'package:path/path.dart';
10 | import 'package:img_syncer/global.dart';
11 |
12 | Timer? autoSyncTimer;
13 |
14 | Future reloadAutoSyncTimer() async {
15 | if (autoSyncTimer != null) {
16 | autoSyncTimer!.cancel();
17 | }
18 | final prefs = await SharedPreferences.getInstance();
19 | final backgroundSyncEnable = prefs.getBool('backgroundSyncEnabled') ?? false;
20 | if (!backgroundSyncEnable) return;
21 | final backgroundSyncInterval =
22 | Duration(minutes: prefs.getInt('backgroundSyncInterval') ?? 60 * 12);
23 | print("backgroundSyncInterval: $backgroundSyncInterval");
24 | autoSyncTimer = Timer.periodic(backgroundSyncInterval, (timer) async {
25 | print("start auto sync");
26 | if (settingModel.localFolder == "" || !settingModel.isRemoteStorageSetted) {
27 | return;
28 | }
29 | final wifiOnly = prefs.getBool('backgroundSyncWifiOnly') ?? true;
30 | if (wifiOnly) {
31 | final result = await Connectivity().checkConnectivity();
32 | if (result != ConnectivityResult.wifi) {
33 | return;
34 | }
35 | }
36 | if (stateModel.isUploading() || stateModel.isDownloading()) return;
37 | await refreshUnsynchronizedPhotos();
38 | Map ids = {};
39 | for (final id in stateModel.notSyncedIDs) {
40 | ids[id] = true;
41 | }
42 | final all = await getPhotos();
43 | for (var asset in all) {
44 | final id = asset.id;
45 | if (ids[id] != true) {
46 | continue;
47 | }
48 | try {
49 | await storage.uploadAssetEntity(asset);
50 | } catch (e) {
51 | print(e);
52 | continue;
53 | }
54 | }
55 | eventBus.fire(RemoteRefreshEvent());
56 | });
57 | }
58 |
59 | Future> getPhotos() async {
60 | List all = [];
61 | final re = await requestPermission();
62 | if (!re) return all;
63 | final List paths =
64 | await PhotoManager.getAssetPathList(type: RequestType.common);
65 | for (var path in paths) {
66 | if (path.name == settingModel.localFolder) {
67 | final newpath = await path.fetchPathProperties(
68 | filterOptionGroup: FilterOptionGroup(
69 | orders: [
70 | const OrderOption(
71 | type: OrderOptionType.createDate,
72 | asc: false,
73 | ),
74 | ],
75 | ));
76 | int assetOffset = 0;
77 | int assetPageSize = 100;
78 | while (true) {
79 | final List assets = await newpath!.getAssetListRange(
80 | start: assetOffset, end: assetOffset + assetPageSize);
81 | if (assets.isEmpty) {
82 | break;
83 | }
84 | all.addAll(assets);
85 | assetOffset += assetPageSize;
86 | }
87 | break;
88 | }
89 | }
90 | return all;
91 | }
92 |
--------------------------------------------------------------------------------
/lib/theme.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | const textThemeLight = TextTheme(
4 | headlineMedium:
5 | TextStyle(fontFamily: 'Ubuntu', color: Color.fromARGB(255, 0, 44, 36)),
6 | bodySmall: TextStyle(fontFamily: 'Ubuntu'),
7 | bodyLarge: TextStyle(fontFamily: 'Ubuntu'),
8 | bodyMedium: TextStyle(fontFamily: 'Ubuntu'),
9 | labelSmall: TextStyle(fontFamily: 'Ubuntu'),
10 | labelLarge: TextStyle(fontFamily: 'Ubuntu'),
11 | labelMedium: TextStyle(fontFamily: 'Ubuntu'),
12 | );
13 |
14 | const textThemeDark = TextTheme(
15 | headlineMedium: TextStyle(
16 | fontFamily: 'Ubuntu', color: Color.fromARGB(255, 172, 196, 192)),
17 | bodySmall: TextStyle(fontFamily: 'Ubuntu'),
18 | bodyLarge: TextStyle(fontFamily: 'Ubuntu'),
19 | bodyMedium: TextStyle(fontFamily: 'Ubuntu'),
20 | labelSmall: TextStyle(fontFamily: 'Ubuntu'),
21 | labelLarge: TextStyle(fontFamily: 'Ubuntu'),
22 | labelMedium: TextStyle(fontFamily: 'Ubuntu'),
23 | );
24 |
25 | const navigationBarThemeLight = NavigationBarThemeData(
26 | height: 67,
27 | iconTheme: MaterialStatePropertyAll(
28 | IconThemeData(color: Color.fromARGB(255, 0, 37, 30)),
29 | ),
30 | labelTextStyle: MaterialStatePropertyAll(
31 | TextStyle(
32 | fontFamily: 'Ubuntu',
33 | fontSize: 16,
34 | color: Color.fromARGB(255, 0, 37, 30),
35 | height: 1,
36 | ),
37 | ),
38 | );
39 |
40 | const navigationBarThemeDark = NavigationBarThemeData(
41 | height: 67,
42 | iconTheme: MaterialStatePropertyAll(
43 | IconThemeData(color: Color.fromARGB(255, 172, 196, 192)),
44 | ),
45 | labelTextStyle: MaterialStatePropertyAll(
46 | TextStyle(
47 | fontFamily: 'Ubuntu',
48 | fontSize: 16,
49 | color: Color.fromARGB(255, 172, 196, 192),
50 | height: 1,
51 | ),
52 | ),
53 | );
54 |
55 | const iconThemeLight = IconThemeData(color: Color.fromARGB(255, 0, 44, 36));
56 | const iconThemeDark = IconThemeData(color: Color.fromARGB(255, 172, 196, 192));
57 |
58 | const floatingActionButtonThemeLight = FloatingActionButtonThemeData(
59 | backgroundColor: Color.fromARGB(255, 180, 227, 219));
60 |
--------------------------------------------------------------------------------
/lib/util.dart:
--------------------------------------------------------------------------------
1 | import 'package:path/path.dart';
2 |
3 | bool isVideoByPath(String path) {
4 | switch (extension(path).toLowerCase()) {
5 | case ".mp4":
6 | case ".avi":
7 | case ".mov":
8 | case ".mkv":
9 | case ".flv":
10 | case ".rmvb":
11 | case ".rm":
12 | case ".3gp":
13 | case ".wmv":
14 | case ".mpeg":
15 | case ".mpg":
16 | case ".webm":
17 | return true;
18 | }
19 | return false;
20 | }
21 |
--------------------------------------------------------------------------------
/lib/video_route.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 | import 'package:flutter/material.dart';
3 | import 'package:img_syncer/asset.dart';
4 | import 'package:chewie/chewie.dart';
5 | import 'package:video_player/video_player.dart';
6 | import 'package:img_syncer/global.dart';
7 |
8 | class VideoRoute extends StatefulWidget {
9 | const VideoRoute({
10 | Key? key,
11 | required this.asset,
12 | }) : super(key: key);
13 | final Asset asset;
14 |
15 | @override
16 | _VideoRouteState createState() => _VideoRouteState();
17 | }
18 |
19 | class _VideoRouteState extends State {
20 | late VideoPlayerController videoPlayerController;
21 | late ChewieController chewieController;
22 | bool isInitialized = false;
23 |
24 | @override
25 | void initState() {
26 | super.initState();
27 | initializePlayer();
28 | }
29 |
30 | @override
31 | void dispose() {
32 | super.dispose();
33 | chewieController.dispose();
34 | videoPlayerController.dispose();
35 | }
36 |
37 | Future initializePlayer() async {
38 | if (widget.asset.hasLocal) {
39 | final file = await widget.asset.local!.originFile;
40 | videoPlayerController = VideoPlayerController.file(file!);
41 | } else if (widget.asset.hasRemote) {
42 | var uri = widget.asset.remote!.path;
43 | if (uri[0] != '/') {
44 | uri = "/$uri";
45 | }
46 | final url = "$httpBaseUrl$uri";
47 | videoPlayerController = VideoPlayerController.network(url);
48 | }
49 | await videoPlayerController.initialize();
50 | Widget customControls = const MaterialControls();
51 | var controlsSafeAreaMinimum = const EdgeInsets.all(0);
52 | if (Platform.isIOS || Platform.isMacOS) {
53 | controlsSafeAreaMinimum = const EdgeInsets.fromLTRB(0, 30, 0, 20);
54 | customControls = const CupertinoControls(
55 | backgroundColor: Color.fromARGB(255, 82, 82, 82),
56 | iconColor: Colors.white);
57 | }
58 | chewieController = ChewieController(
59 | videoPlayerController: videoPlayerController,
60 | autoPlay: true,
61 | looping: false,
62 | showControlsOnInitialize: false,
63 | showOptions: false,
64 | customControls: customControls,
65 | allowFullScreen: false,
66 | allowMuting: false,
67 | controlsSafeAreaMinimum: controlsSafeAreaMinimum,
68 | );
69 | setState(() {
70 | isInitialized = true;
71 | });
72 | }
73 |
74 | @override
75 | Widget build(BuildContext context) {
76 | return Scaffold(
77 | backgroundColor: Colors.black,
78 | body: Stack(
79 | children: [
80 | Center(
81 | child: isInitialized
82 | ? Container(
83 | // padding: const EdgeInsets.fromLTRB(0, 80, 0, 20),
84 | child: Chewie(
85 | controller: chewieController,
86 | ),
87 | )
88 | : const CircularProgressIndicator(),
89 | ),
90 | Positioned(
91 | top: 0,
92 | left: 0,
93 | right: 0,
94 | child: AppBar(
95 | backgroundColor: const Color(0x00000000),
96 | iconTheme: const IconThemeData(color: Colors.white),
97 | ),
98 | ),
99 | ],
100 | ));
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/linux/.gitignore:
--------------------------------------------------------------------------------
1 | flutter/ephemeral
2 |
--------------------------------------------------------------------------------
/linux/flutter/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # This file controls Flutter-level build steps. It should not be edited.
2 | cmake_minimum_required(VERSION 3.10)
3 |
4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
5 |
6 | # Configuration provided via flutter tool.
7 | include(${EPHEMERAL_DIR}/generated_config.cmake)
8 |
9 | # TODO: Move the rest of this into files in ephemeral. See
10 | # https://github.com/flutter/flutter/issues/57146.
11 |
12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...),
13 | # which isn't available in 3.10.
14 | function(list_prepend LIST_NAME PREFIX)
15 | set(NEW_LIST "")
16 | foreach(element ${${LIST_NAME}})
17 | list(APPEND NEW_LIST "${PREFIX}${element}")
18 | endforeach(element)
19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
20 | endfunction()
21 |
22 | # === Flutter Library ===
23 | # System-level dependencies.
24 | find_package(PkgConfig REQUIRED)
25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
28 |
29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
30 |
31 | # Published to parent scope for install step.
32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
36 |
37 | list(APPEND FLUTTER_LIBRARY_HEADERS
38 | "fl_basic_message_channel.h"
39 | "fl_binary_codec.h"
40 | "fl_binary_messenger.h"
41 | "fl_dart_project.h"
42 | "fl_engine.h"
43 | "fl_json_message_codec.h"
44 | "fl_json_method_codec.h"
45 | "fl_message_codec.h"
46 | "fl_method_call.h"
47 | "fl_method_channel.h"
48 | "fl_method_codec.h"
49 | "fl_method_response.h"
50 | "fl_plugin_registrar.h"
51 | "fl_plugin_registry.h"
52 | "fl_standard_message_codec.h"
53 | "fl_standard_method_codec.h"
54 | "fl_string_codec.h"
55 | "fl_value.h"
56 | "fl_view.h"
57 | "flutter_linux.h"
58 | )
59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
60 | add_library(flutter INTERFACE)
61 | target_include_directories(flutter INTERFACE
62 | "${EPHEMERAL_DIR}"
63 | )
64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
65 | target_link_libraries(flutter INTERFACE
66 | PkgConfig::GTK
67 | PkgConfig::GLIB
68 | PkgConfig::GIO
69 | )
70 | add_dependencies(flutter flutter_assemble)
71 |
72 | # === Flutter tool backend ===
73 | # _phony_ is a non-existent file to force this command to run every time,
74 | # since currently there's no way to get a full input/output list from the
75 | # flutter tool.
76 | add_custom_command(
77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_
79 | COMMAND ${CMAKE_COMMAND} -E env
80 | ${FLUTTER_TOOL_ENVIRONMENT}
81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
83 | VERBATIM
84 | )
85 | add_custom_target(flutter_assemble DEPENDS
86 | "${FLUTTER_LIBRARY}"
87 | ${FLUTTER_LIBRARY_HEADERS}
88 | )
89 |
--------------------------------------------------------------------------------
/linux/flutter/generated_plugin_registrant.cc:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // clang-format off
6 |
7 | #include "generated_plugin_registrant.h"
8 |
9 | #include
10 | #include
11 | #include
12 |
13 | void fl_register_plugins(FlPluginRegistry* registry) {
14 | g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
15 | fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
16 | dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
17 | g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
18 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
19 | file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
20 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
21 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
22 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
23 | }
24 |
--------------------------------------------------------------------------------
/linux/flutter/generated_plugin_registrant.h:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // clang-format off
6 |
7 | #ifndef GENERATED_PLUGIN_REGISTRANT_
8 | #define GENERATED_PLUGIN_REGISTRANT_
9 |
10 | #include
11 |
12 | // Registers Flutter plugins.
13 | void fl_register_plugins(FlPluginRegistry* registry);
14 |
15 | #endif // GENERATED_PLUGIN_REGISTRANT_
16 |
--------------------------------------------------------------------------------
/linux/flutter/generated_plugins.cmake:
--------------------------------------------------------------------------------
1 | #
2 | # Generated file, do not edit.
3 | #
4 |
5 | list(APPEND FLUTTER_PLUGIN_LIST
6 | dynamic_color
7 | file_selector_linux
8 | url_launcher_linux
9 | )
10 |
11 | list(APPEND FLUTTER_FFI_PLUGIN_LIST
12 | )
13 |
14 | set(PLUGIN_BUNDLED_LIBRARIES)
15 |
16 | foreach(plugin ${FLUTTER_PLUGIN_LIST})
17 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
18 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
21 | endforeach(plugin)
22 |
23 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
26 | endforeach(ffi_plugin)
27 |
--------------------------------------------------------------------------------
/linux/main.cc:
--------------------------------------------------------------------------------
1 | #include "my_application.h"
2 |
3 | int main(int argc, char** argv) {
4 | g_autoptr(MyApplication) app = my_application_new();
5 | return g_application_run(G_APPLICATION(app), argc, argv);
6 | }
7 |
--------------------------------------------------------------------------------
/linux/my_application.cc:
--------------------------------------------------------------------------------
1 | #include "my_application.h"
2 |
3 | #include
4 | #ifdef GDK_WINDOWING_X11
5 | #include
6 | #endif
7 |
8 | #include "flutter/generated_plugin_registrant.h"
9 |
10 | struct _MyApplication {
11 | GtkApplication parent_instance;
12 | char** dart_entrypoint_arguments;
13 | };
14 |
15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
16 |
17 | // Implements GApplication::activate.
18 | static void my_application_activate(GApplication* application) {
19 | MyApplication* self = MY_APPLICATION(application);
20 | GtkWindow* window =
21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
22 |
23 | // Use a header bar when running in GNOME as this is the common style used
24 | // by applications and is the setup most users will be using (e.g. Ubuntu
25 | // desktop).
26 | // If running on X and not using GNOME then just use a traditional title bar
27 | // in case the window manager does more exotic layout, e.g. tiling.
28 | // If running on Wayland assume the header bar will work (may need changing
29 | // if future cases occur).
30 | gboolean use_header_bar = TRUE;
31 | #ifdef GDK_WINDOWING_X11
32 | GdkScreen* screen = gtk_window_get_screen(window);
33 | if (GDK_IS_X11_SCREEN(screen)) {
34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
36 | use_header_bar = FALSE;
37 | }
38 | }
39 | #endif
40 | if (use_header_bar) {
41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
42 | gtk_widget_show(GTK_WIDGET(header_bar));
43 | gtk_header_bar_set_title(header_bar, "img_syncer");
44 | gtk_header_bar_set_show_close_button(header_bar, TRUE);
45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
46 | } else {
47 | gtk_window_set_title(window, "img_syncer");
48 | }
49 |
50 | gtk_window_set_default_size(window, 1280, 720);
51 | gtk_widget_show(GTK_WIDGET(window));
52 |
53 | g_autoptr(FlDartProject) project = fl_dart_project_new();
54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
55 |
56 | FlView* view = fl_view_new(project);
57 | gtk_widget_show(GTK_WIDGET(view));
58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
59 |
60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view));
61 |
62 | gtk_widget_grab_focus(GTK_WIDGET(view));
63 | }
64 |
65 | // Implements GApplication::local_command_line.
66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
67 | MyApplication* self = MY_APPLICATION(application);
68 | // Strip out the first argument as it is the binary name.
69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
70 |
71 | g_autoptr(GError) error = nullptr;
72 | if (!g_application_register(application, nullptr, &error)) {
73 | g_warning("Failed to register: %s", error->message);
74 | *exit_status = 1;
75 | return TRUE;
76 | }
77 |
78 | g_application_activate(application);
79 | *exit_status = 0;
80 |
81 | return TRUE;
82 | }
83 |
84 | // Implements GObject::dispose.
85 | static void my_application_dispose(GObject* object) {
86 | MyApplication* self = MY_APPLICATION(object);
87 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
88 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
89 | }
90 |
91 | static void my_application_class_init(MyApplicationClass* klass) {
92 | G_APPLICATION_CLASS(klass)->activate = my_application_activate;
93 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
94 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
95 | }
96 |
97 | static void my_application_init(MyApplication* self) {}
98 |
99 | MyApplication* my_application_new() {
100 | return MY_APPLICATION(g_object_new(my_application_get_type(),
101 | "application-id", APPLICATION_ID,
102 | "flags", G_APPLICATION_NON_UNIQUE,
103 | nullptr));
104 | }
105 |
--------------------------------------------------------------------------------
/linux/my_application.h:
--------------------------------------------------------------------------------
1 | #ifndef FLUTTER_MY_APPLICATION_H_
2 | #define FLUTTER_MY_APPLICATION_H_
3 |
4 | #include
5 |
6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
7 | GtkApplication)
8 |
9 | /**
10 | * my_application_new:
11 | *
12 | * Creates a new Flutter-based application.
13 | *
14 | * Returns: a new #MyApplication.
15 | */
16 | MyApplication* my_application_new();
17 |
18 | #endif // FLUTTER_MY_APPLICATION_H_
19 |
--------------------------------------------------------------------------------
/macos/.gitignore:
--------------------------------------------------------------------------------
1 | # Flutter-related
2 | **/Flutter/ephemeral/
3 | **/Pods/
4 |
5 | # Xcode-related
6 | **/dgph
7 | **/xcuserdata/
8 |
--------------------------------------------------------------------------------
/macos/Flutter/Flutter-Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "ephemeral/Flutter-Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/macos/Flutter/Flutter-Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "ephemeral/Flutter-Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/macos/Flutter/GeneratedPluginRegistrant.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | import FlutterMacOS
6 | import Foundation
7 |
8 | import connectivity_plus
9 | import device_info_plus
10 | import dynamic_color
11 | import file_selector_macos
12 | import package_info_plus
13 | import path_provider_foundation
14 | import photo_manager
15 | import share_plus
16 | import shared_preferences_foundation
17 | import url_launcher_macos
18 | import wakelock_plus
19 |
20 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
21 | ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
22 | DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
23 | DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
24 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
25 | FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
26 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
27 | PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin"))
28 | SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
29 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
30 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
31 | WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
32 | }
33 |
--------------------------------------------------------------------------------
/macos/Podfile:
--------------------------------------------------------------------------------
1 | platform :osx, '10.14'
2 |
3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
5 |
6 | project 'Runner', {
7 | 'Debug' => :debug,
8 | 'Profile' => :release,
9 | 'Release' => :release,
10 | }
11 |
12 | def flutter_root
13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
14 | unless File.exist?(generated_xcode_build_settings_path)
15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
16 | end
17 |
18 | File.foreach(generated_xcode_build_settings_path) do |line|
19 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
20 | return matches[1].strip if matches
21 | end
22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
23 | end
24 |
25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
26 |
27 | flutter_macos_podfile_setup
28 |
29 | target 'Runner' do
30 | use_frameworks!
31 | use_modular_headers!
32 |
33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
34 | target 'RunnerTests' do
35 | inherit! :search_paths
36 | end
37 | end
38 |
39 | post_install do |installer|
40 | installer.pods_project.targets.each do |target|
41 | flutter_additional_macos_build_settings(target)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/macos/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/macos/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
3 |
4 | @NSApplicationMain
5 | class AppDelegate: FlutterAppDelegate {
6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
7 | return true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "app_icon_16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "app_icon_32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "app_icon_32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "app_icon_64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "app_icon_128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "app_icon_256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "app_icon_256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "app_icon_512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "app_icon_512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "app_icon_1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
--------------------------------------------------------------------------------
/macos/Runner/Configs/AppInfo.xcconfig:
--------------------------------------------------------------------------------
1 | // Application-level settings for the Runner target.
2 | //
3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
4 | // future. If not, the values below would default to using the project name when this becomes a
5 | // 'flutter create' template.
6 |
7 | // The application's name. By default this is also the title of the Flutter window.
8 | PRODUCT_NAME = img_syncer
9 |
10 | // The application's bundle identifier
11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.imgSyncer
12 |
13 | // The copyright displayed in application information
14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved.
15 |
--------------------------------------------------------------------------------
/macos/Runner/Configs/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Debug.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/macos/Runner/Configs/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Release.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/macos/Runner/Configs/Warnings.xcconfig:
--------------------------------------------------------------------------------
1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
2 | GCC_WARN_UNDECLARED_SELECTOR = YES
3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
6 | CLANG_WARN_PRAGMA_PACK = YES
7 | CLANG_WARN_STRICT_PROTOTYPES = YES
8 | CLANG_WARN_COMMA = YES
9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES
10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
12 | GCC_WARN_SHADOW = YES
13 | CLANG_WARN_UNREACHABLE_CODE = YES
14 |
--------------------------------------------------------------------------------
/macos/Runner/DebugProfile.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.cs.allow-jit
8 |
9 | com.apple.security.network.server
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/macos/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | $(PRODUCT_COPYRIGHT)
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/macos/Runner/MainFlutterWindow.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
3 |
4 | class MainFlutterWindow: NSWindow {
5 | override func awakeFromNib() {
6 | let flutterViewController = FlutterViewController.init()
7 | let windowFrame = self.frame
8 | self.contentViewController = flutterViewController
9 | self.setFrame(windowFrame, display: true)
10 |
11 | RegisterGeneratedPlugins(registry: flutterViewController)
12 |
13 | super.awakeFromNib()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/macos/Runner/Release.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/proto/img_syncer.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | package img_syncer;
3 |
4 | option go_package = "github.com/fregie/img_syncer/proto";
5 |
6 | service ImgSyncer {
7 | rpc ListByDate (ListByDateRequest) returns (ListByDateResponse) {}
8 | rpc Delete (DeleteRequest) returns (DeleteResponse) {}
9 | rpc FilterNotUploaded (stream FilterNotUploadedRequest) returns (stream FilterNotUploadedResponse) {}
10 | // SAMBA Drive
11 | rpc SetDriveSMB (SetDriveSMBRequest) returns (SetDriveSMBResponse) {}
12 | rpc ListDriveSMBShares (ListDriveSMBSharesRequest) returns (ListDriveSMBSharesResponse) {}
13 | rpc ListDriveSMBDir (ListDriveSMBDirRequest) returns (ListDriveSMBDirResponse) {}
14 | rpc SetDriveSMBShare (SetDriveSMBShareRequest) returns (SetDriveSMBShareResponse) {}
15 | // Webdav Drive
16 | rpc SetDriveWebdav (SetDriveWebdavRequest) returns (SetDriveWebdavResponse) {}
17 | rpc ListDriveWebdavDir (ListDriveWebdavDirRequest) returns (ListDriveWebdavDirResponse) {}
18 | // NFS Drive
19 | rpc SetDriveNFS (SetDriveNFSRequest) returns (SetDriveNFSResponse) {}
20 | rpc ListDriveNFSDir (ListDriveNFSDirRequest) returns (ListDriveNFSDirResponse) {}
21 | rpc SetDriveBaiduNetDisk (SetDriveBaiduNetDiskRequest) returns (SetDriveBaiduNetDiskResponse) {}
22 | rpc StartBaiduNetdiskLogin (StartBaiduNetdiskLoginRequest) returns (StartBaiduNetdiskLoginResponse) {}
23 | }
24 |
25 | // enum ContentType {
26 | // IMAGE_JPEG = 0;
27 | // IMAGE_PNG = 1;
28 | // IMAGE_GIF = 2;
29 | // }
30 |
31 | message ListByDateRequest {
32 | string date = 1; // YYYY:MM:DD
33 | int32 offset = 2;
34 | int32 maxReturn = 3;
35 | }
36 | message ListByDateResponse {
37 | bool success = 1;
38 | string message = 2;
39 | repeated string paths = 3;
40 | }
41 |
42 | message DeleteRequest {
43 | repeated string paths = 1;
44 | }
45 | message DeleteResponse {
46 | bool success = 1;
47 | string message = 2;
48 | }
49 |
50 | message FilterNotUploadedRequestInfo {
51 | string name = 1;
52 | string date = 2;
53 | string id = 3;
54 | }
55 |
56 | message FilterNotUploadedRequest {
57 | repeated FilterNotUploadedRequestInfo photos = 1;
58 | bool isFinished = 2;
59 | }
60 | message FilterNotUploadedResponse {
61 | bool success = 1;
62 | string message = 2;
63 | repeated string notUploaedIDs = 3;
64 | bool isFinished = 4;
65 | }
66 |
67 | message SetDriveSMBRequest {
68 | string addr = 1;
69 | string username = 2;
70 | string password = 3;
71 | string share = 4;
72 | string root = 5;
73 | }
74 | message SetDriveSMBResponse {
75 | bool success = 1;
76 | string message = 2;
77 | }
78 |
79 | message ListDriveSMBSharesRequest {}
80 | message ListDriveSMBSharesResponse {
81 | bool success = 1;
82 | string message = 2;
83 | repeated string shares = 3;
84 | }
85 |
86 | message ListDriveSMBDirRequest {
87 | string share = 1;
88 | string dir = 2;
89 | }
90 | message ListDriveSMBDirResponse {
91 | bool success = 1;
92 | string message = 2;
93 | repeated string dirs = 3;
94 | }
95 |
96 | message SetDriveSMBShareRequest {
97 | string share = 1;
98 | string root = 2;
99 | }
100 | message SetDriveSMBShareResponse {
101 | bool success = 1;
102 | string message = 2;
103 | }
104 |
105 | message SetDriveWebdavRequest {
106 | string addr = 1;
107 | string username = 2;
108 | string password = 3;
109 | string root = 4;
110 | }
111 | message SetDriveWebdavResponse {
112 | bool success = 1;
113 | string message = 2;
114 | }
115 |
116 | message ListDriveWebdavDirRequest {
117 | string dir = 1;
118 | }
119 | message ListDriveWebdavDirResponse {
120 | bool success = 1;
121 | string message = 2;
122 | repeated string dirs = 3;
123 | }
124 |
125 | message SetDriveNFSRequest {
126 | string addr = 1;
127 | string root = 2;
128 | }
129 | message SetDriveNFSResponse {
130 | bool success = 1;
131 | string message = 2;
132 | }
133 |
134 | message ListDriveNFSDirRequest {
135 | string dir = 1;
136 | }
137 | message ListDriveNFSDirResponse {
138 | bool success = 1;
139 | string message = 2;
140 | repeated string dirs = 3;
141 | }
142 |
143 | message SetDriveBaiduNetDiskRequest {
144 | string refreshToken = 1;
145 | string accessToken = 2;
146 | string tmpDir = 3;
147 | }
148 | message SetDriveBaiduNetDiskResponse {
149 | bool success = 1;
150 | string message = 2;
151 | }
152 |
153 | message StartBaiduNetdiskLoginRequest {
154 | string tmpDir = 1;
155 | }
156 | message StartBaiduNetdiskLoginResponse {
157 | bool success = 1;
158 | string message = 2;
159 | string refreshToken = 3;
160 | string accessToken = 4;
161 | int64 exiresAt = 5;
162 | }
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: img_syncer
2 | description: A new Flutter project.
3 | # The following line prevents the package from being accidentally published to
4 | # pub.dev using `flutter pub publish`. This is preferred for private packages.
5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev
6 |
7 | # The following defines the version and build number for your application.
8 | # A version number is three numbers separated by dots, like 1.2.43
9 | # followed by an optional build number separated by a +.
10 | # Both the version and the builder number may be overridden in flutter
11 | # build by specifying --build-name and --build-number, respectively.
12 | # In Android, build-name is used as versionName while build-number used as versionCode.
13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning
14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
15 | # Read more about iOS versioning at
16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 | # In Windows, build-name is used as the major, minor, and patch parts
18 | # of the product and file versions while build-number is used as the build suffix.
19 | version: 1.3.4+18
20 |
21 | environment:
22 | sdk: '>=2.19.1 <3.0.0'
23 |
24 | # Dependencies specify other packages that your package needs in order to work.
25 | # To automatically upgrade your package dependencies to the latest versions
26 | # consider running `flutter pub upgrade --major-versions`. Alternatively,
27 | # dependencies can be manually updated by changing the version numbers below to
28 | # the latest version available on pub.dev. To see which dependencies have newer
29 | # versions available, run `flutter pub outdated`.
30 | dependencies:
31 | flutter:
32 | sdk: flutter
33 |
34 | # The following adds the Cupertino Icons font to your application.
35 | # Use with the CupertinoIcons class for iOS style icons.
36 | cupertino_icons: ^1.0.2
37 | photo_album_manager: ^1.2.0
38 | toast: ^0.3.0
39 | grpc: ^3.1.0
40 | # photo_manager: ^2.6.0
41 | photo_manager:
42 | git:
43 | url: git@github.com:fluttercandies/flutter_photo_manager.git
44 | ref: main
45 | photo_view:
46 | git:
47 | url: git@github.com:bluefireteam/photo_view.git
48 | ref: main
49 | date_format: ^2.0.7
50 | image_picker: ^1.0.0
51 | provider: ^6.0.5
52 | path: ^1.8.2
53 | shared_preferences: ^2.0.18
54 | event_bus: ^2.0.0
55 | exif: ^3.1.4
56 | adaptive_theme: ^3.2.0
57 | dynamic_color: ^1.6.2
58 | synchronized: ^3.0.1
59 | rxdart: ^0.27.7
60 | image: ^4.0.17
61 | share_plus: ^7.0.2
62 | logger: ^1.3.0
63 | connectivity_plus: ^4.0.1
64 | flutter_launcher_icons: ^0.13.1
65 | extended_image: ^8.0.2
66 | flutter_localizations:
67 | sdk: flutter
68 | chewie: ^1.7.0
69 | path_provider: ^2.0.15
70 | mime: ^1.0.4
71 | url_launcher: ^6.1.11
72 | flutter_image_compress: ^2.0.3
73 | gallery_saver: ^2.3.2
74 | vibration: ^1.8.1
75 |
76 | dev_dependencies:
77 | flutter_test:
78 | sdk: flutter
79 |
80 | # The "flutter_lints" package below contains a set of recommended lints to
81 | # encourage good coding practices. The lint set provided by the package is
82 | # activated in the `analysis_options.yaml` file located at the root of your
83 | # package. See that file for information about deactivating specific lint
84 | # rules and activating additional ones.
85 | flutter_lints: ^2.0.0
86 |
87 | # For information on the generic Dart part of this file, see the
88 | # following page: https://dart.dev/tools/pub/pubspec
89 |
90 | # The following section is specific to Flutter packages.
91 | flutter:
92 |
93 | # The following line ensures that the Material Icons font is
94 | # included with your application, so that you can use the icons in
95 | # the material Icons class.
96 | uses-material-design: true
97 | generate: true
98 | # To add assets to your application, add an assets section, like this:
99 | assets:
100 | - assets/images/broken.png
101 | - assets/images/gray.jpg
102 | - assets/icon/pho_icon.png
103 |
104 | # An image asset can refer to one or more resolution-specific "variants", see
105 | # https://flutter.dev/assets-and-images/#resolution-aware
106 |
107 | # For details regarding adding assets from package dependencies, see
108 | # https://flutter.dev/assets-and-images/#from-packages
109 |
110 | fonts:
111 | - family: Ubuntu
112 | fonts:
113 | - asset: assets/fonts/Ubuntu-M.ttf
114 | - family: Ubuntu-thin
115 | fonts:
116 | - asset: assets/fonts/Ubuntu-Th.ttf
117 | - family: Ubuntu-condensed
118 | fonts:
119 | - asset: assets/fonts/Ubuntu-C.ttf
120 |
121 | flutter_icons:
122 | android: "launcher_icon"
123 | ios: true
124 | image_path: "assets/icon/pho_icon.png"
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:stable-slim
2 | RUN \
3 | apt-get update && \
4 | apt-get install -y ca-certificates && \
5 | apt-get clean
6 |
7 | ADD output/img_syncer_server /server
8 |
9 | WORKDIR /
10 | CMD ["bash", "-c", "/server"]
--------------------------------------------------------------------------------
/server/api/baidu.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "sync"
10 | "time"
11 |
12 | login_success_html "github.com/fregie/img_syncer/assets/html/login_success"
13 | pb "github.com/fregie/img_syncer/proto"
14 | baidu "github.com/fregie/img_syncer/server/drive/baidu"
15 | )
16 |
17 | type authRsp struct {
18 | Error string `json:"error"`
19 | ErrDesc string `json:"error_description"`
20 | RefreshToken string `json:"refresh_token"`
21 | AccessToken string `json:"access_token"`
22 | ExpiresIn int `json:"expires_in"`
23 | }
24 |
25 | func (a *api) SetDriveBaiduNetDisk(ctx context.Context, req *pb.SetDriveBaiduNetDiskRequest) (rsp *pb.SetDriveBaiduNetDiskResponse, e error) {
26 | rsp = &pb.SetDriveBaiduNetDiskResponse{Success: true}
27 | if req.RefreshToken == "" {
28 | rsp.Success, rsp.Message = false, "param error: refresh token is empty"
29 | return
30 | }
31 | d, err := baidu.NewBaiduNetdiskDrive(req.RefreshToken, req.AccessToken)
32 | if err != nil {
33 | rsp.Success, rsp.Message = false, err.Error()
34 | return
35 | }
36 | if req.TmpDir != "" {
37 | d.SetTmpDir(req.TmpDir)
38 | }
39 | a.im.SetDrive(d)
40 | return
41 | }
42 |
43 | func (a *api) StartBaiduNetdiskLogin(ctx context.Context, req *pb.StartBaiduNetdiskLoginRequest) (rsp *pb.StartBaiduNetdiskLoginResponse, e error) {
44 | rsp = &pb.StartBaiduNetdiskLoginResponse{Success: true}
45 | if a.baiduLogginInChan != nil {
46 | rsp.Success, rsp.Message = false, "login in progress"
47 | return
48 | }
49 | newCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
50 | defer cancel()
51 | a.baiduLogginInChan = make(chan *pb.StartBaiduNetdiskLoginResponse)
52 | select {
53 | case <-newCtx.Done():
54 | rsp.Success, rsp.Message = false, "login timeout"
55 | case rsp = <-a.baiduLogginInChan:
56 | }
57 | if rsp.Success && req.TmpDir != "" {
58 | d := a.im.Drive()
59 | baidu, ok := d.(*baidu.BaiduNetdisk)
60 | if ok {
61 | baidu.SetTmpDir(req.TmpDir)
62 | }
63 | }
64 | close(a.baiduLogginInChan)
65 | a.baiduLogginInChan = nil
66 | return
67 | }
68 |
69 | var finishLock sync.Mutex
70 |
71 | func (a *api) finishBaiduLogin(rsp *pb.StartBaiduNetdiskLoginResponse) {
72 | finishLock.Lock()
73 | defer finishLock.Unlock()
74 | if a.baiduLogginInChan != nil {
75 | a.baiduLogginInChan <- rsp
76 | }
77 | }
78 |
79 | func (a *api) httpBaiduCallback(w http.ResponseWriter, r *http.Request) {
80 | loginRsp := &pb.StartBaiduNetdiskLoginResponse{Success: true}
81 | var err error
82 | defer func() {
83 | if err != nil {
84 | loginRsp.Success, loginRsp.Message = false, err.Error()
85 | }
86 | a.finishBaiduLogin(loginRsp)
87 | }()
88 | code := r.URL.Query().Get("code")
89 | if code == "" {
90 | w.WriteHeader(http.StatusBadRequest)
91 | w.Write([]byte("code is empty"))
92 | return
93 | }
94 | reqUrl := fmt.Sprintf("https://openapi.baidu.com/oauth/2.0/token?grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s&redirect_uri=http://localhost.pho.tools:%d/baidu/callback", code, baidu.PhoAppKey, baidu.PhoSecretKey, a.httpPort)
95 | req, err := http.NewRequest(http.MethodGet, reqUrl, nil)
96 | if err != nil {
97 | w.WriteHeader(http.StatusInternalServerError)
98 | w.Write([]byte(err.Error()))
99 | return
100 | }
101 | req.Header.Set("User-Agent", "pan.baidu.com")
102 | resp, err := http.DefaultClient.Do(req)
103 | if err != nil {
104 | w.WriteHeader(http.StatusInternalServerError)
105 | w.Write([]byte(err.Error()))
106 | return
107 | }
108 | defer resp.Body.Close()
109 | body, err := io.ReadAll(resp.Body)
110 | if err != nil {
111 | w.WriteHeader(http.StatusInternalServerError)
112 | w.Write([]byte(err.Error()))
113 | return
114 | }
115 | var auth authRsp
116 | err = json.Unmarshal(body, &auth)
117 | if err != nil {
118 | w.WriteHeader(http.StatusInternalServerError)
119 | w.Write([]byte(err.Error()))
120 | return
121 | }
122 | if auth.Error != "" {
123 | w.WriteHeader(http.StatusInternalServerError)
124 | w.Write([]byte(auth.ErrDesc))
125 | return
126 | }
127 | d, err := baidu.NewBaiduNetdiskDrive(auth.RefreshToken, auth.AccessToken)
128 | if err != nil {
129 | w.WriteHeader(http.StatusInternalServerError)
130 | w.Write([]byte(err.Error()))
131 | return
132 | }
133 | loginRsp.RefreshToken = auth.RefreshToken
134 | loginRsp.AccessToken = auth.AccessToken
135 | loginRsp.ExiresAt = int64(auth.ExpiresIn) + time.Now().Unix()
136 | a.im.SetDrive(d)
137 | w.Write(login_success_html.Html)
138 | }
139 |
--------------------------------------------------------------------------------
/server/api/img.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "path/filepath"
8 | "time"
9 |
10 | pb "github.com/fregie/img_syncer/proto"
11 | "github.com/fregie/img_syncer/server/imgmanager"
12 | )
13 |
14 | type api struct {
15 | im *imgmanager.ImgManager
16 | httpPort int
17 | baiduLogginInChan chan *pb.StartBaiduNetdiskLoginResponse
18 |
19 | pb.UnimplementedImgSyncerServer
20 | }
21 |
22 | func NewApi(im *imgmanager.ImgManager) *api {
23 | a := &api{
24 | im: im,
25 | }
26 | return a
27 | }
28 |
29 | func (a *api) ListByDate(ctx context.Context, req *pb.ListByDateRequest) (rsp *pb.ListByDateResponse, err error) {
30 | rsp = &pb.ListByDateResponse{Success: true}
31 | if req.MaxReturn <= 0 {
32 | req.MaxReturn = 100
33 | }
34 | if req.Offset <= 0 {
35 | req.Offset = 0
36 | }
37 | var e error
38 | start := time.Now()
39 | if req.Date != "" {
40 | start, e = time.Parse("2006:01:02", req.Date)
41 | if e != nil {
42 | rsp.Success, rsp.Message = false, fmt.Sprintf("param error: date format error: %s", req.Date)
43 | return
44 | }
45 | }
46 | rsp.Paths = make([]string, 0, req.MaxReturn)
47 | offset := req.Offset
48 | needReturn := req.MaxReturn
49 | e = a.im.RangeByDate(start, func(path string, size int64) bool {
50 | if offset > 0 {
51 | offset--
52 | return true
53 | }
54 | rsp.Paths = append(rsp.Paths, path)
55 | needReturn--
56 | return needReturn > 0
57 | })
58 | if e != nil {
59 | rsp.Success, rsp.Message = false, e.Error()
60 | return
61 | }
62 | return
63 | }
64 |
65 | func (a *api) Delete(ctx context.Context, req *pb.DeleteRequest) (rsp *pb.DeleteResponse, err error) {
66 | rsp = &pb.DeleteResponse{Success: true}
67 | a.im.DeleteImg(req.Paths)
68 | return
69 | }
70 |
71 | func (a *api) FilterNotUploaded(stream pb.ImgSyncer_FilterNotUploadedServer) error {
72 | all := make(map[string]bool)
73 | a.im.RangeByDate(time.Now(), func(path string, size int64) bool {
74 | name := filepath.Base(path)
75 | all[name] = true
76 | return true
77 | })
78 | for {
79 | r, err := stream.Recv()
80 | if err != nil {
81 | if err == io.EOF {
82 | break
83 | }
84 | return err
85 | }
86 | rsp := &pb.FilterNotUploadedResponse{Success: true, IsFinished: r.IsFinished}
87 | rsp.NotUploaedIDs = make([]string, 0, len(r.Photos))
88 | for _, info := range r.Photos {
89 | t, err := time.Parse("2006:01:02 15:04:05", info.Date)
90 | if err != nil {
91 | continue
92 | }
93 | if !all[encodeName(t, info.Name)] {
94 | rsp.NotUploaedIDs = append(rsp.NotUploaedIDs, info.Id)
95 | }
96 | }
97 | if err := stream.Send(rsp); err != nil {
98 | return err
99 | }
100 | if rsp.IsFinished {
101 | break
102 | }
103 | }
104 | return nil
105 | }
106 |
107 | // func (a *api) FilterNotUploaded(ctx context.Context, req *pb.FilterNotUploadedRequest) (rsp *pb.FilterNotUploadedResponse, err error) {
108 | // rsp = &pb.FilterNotUploadedResponse{Success: true}
109 | // if len(req.Photos) == 0 {
110 | // rsp.Success, rsp.Message = false, "param error: names is empty"
111 | // return
112 | // }
113 | // all := make(map[string]bool)
114 | // a.im.RangeByDate(time.Now(), func(path string, size int64) bool {
115 | // name := filepath.Base(path)
116 | // all[name] = true
117 | // return true
118 | // })
119 | // rsp.NotUploaedIDs = make([]string, 0, 100)
120 | // for _, info := range req.Photos {
121 | // t, err := time.Parse("2006:01:02 15:04:05", info.Date)
122 | // if err != nil {
123 | // continue
124 | // }
125 | // if !all[encodeName(t, info.Name)] {
126 | // rsp.NotUploaedIDs = append(rsp.NotUploaedIDs, info.Id)
127 | // }
128 | // }
129 | // return
130 | // }
131 |
132 | func isVideo(name string) bool {
133 | ext := filepath.Ext(name)
134 | switch ext {
135 | case ".mp4", ".avi", ".rmvb", ".rm", ".flv", ".wmv", ".mkv", ".mov", ".mpg", ".mpeg", ".3gp", ".3g2", ".asf", ".asx", ".vob", ".m2ts", ".mts", ".ts":
136 | return true
137 | }
138 | return false
139 | }
140 |
--------------------------------------------------------------------------------
/server/api/img_test.go:
--------------------------------------------------------------------------------
1 | package api_test
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "testing"
10 | "time"
11 |
12 | pb "github.com/fregie/img_syncer/proto"
13 | "github.com/fregie/img_syncer/test/static"
14 | "github.com/stretchr/testify/suite"
15 | "google.golang.org/grpc"
16 | )
17 |
18 | type ImageTestSuite struct {
19 | suite.Suite
20 | srv pb.ImgSyncerClient
21 | }
22 |
23 | func TestImageTestSuite(t *testing.T) {
24 | suite.Run(t, new(ImageTestSuite))
25 | }
26 |
27 | func (s *ImageTestSuite) SetupTest() {
28 | err := cleanSmb()
29 | s.Nilf(err, "failed to clean smb share: %s", err)
30 | err = initSmbDir()
31 | s.Nilf(err, "failed to init smb dir: %s", err)
32 | grpcConn, err := grpc.Dial(grpcAddr, grpc.WithInsecure())
33 | s.Nil(err)
34 | s.srv = pb.NewImgSyncerClient(grpcConn)
35 | s.setupSmbDrive()
36 | }
37 |
38 | func (s *ImageTestSuite) setupSmbDrive() {
39 | rsp1, err := s.srv.SetDriveSMB(context.Background(), &pb.SetDriveSMBRequest{
40 | Addr: smbSrvAddr,
41 | Username: smbUser,
42 | Password: smbPass,
43 | Share: smbShare,
44 | Root: smbRootDir,
45 | })
46 | s.Nil(err)
47 | s.True(rsp1.Success)
48 | }
49 |
50 | func (s *ImageTestSuite) TestUploadGet() {
51 | ctx := context.Background()
52 | err := s.uploadPic1(ctx)
53 | s.Nil(err)
54 | s.Nil(waitfile(s.srv, pic1ShouldPath, 5*time.Second))
55 | data, err := s.get(ctx, pic1ShouldPath)
56 | s.Nilf(err, "get pic failed: %v", err)
57 | s.Equal(len(static.Pic1), len(data))
58 | }
59 |
60 | func (s *ImageTestSuite) TestGetThumnail() {
61 | ctx := context.Background()
62 | err := s.uploadPic1(ctx)
63 | s.Nil(err)
64 | s.Nil(waitfile(s.srv, pic1ShouldPath, 5*time.Second))
65 | data, err := s.get(ctx, pic1ShouldPath)
66 | s.Nilf(err, "get pic failed: %v", err)
67 | s.Equal(static.Pic1, data)
68 | resp, err := http.Get(fmt.Sprintf("http://%s/thumbnail/%s", httpAddr, pic1ShouldPath))
69 | s.Nilf(err, "get thumbnail failed: %v", err)
70 | defer resp.Body.Close()
71 | body, err := io.ReadAll(resp.Body)
72 | s.Nilf(err, "read thumbnail failed: %v", err)
73 | s.Equalf(http.StatusOK, resp.StatusCode, "body: %s", body)
74 | s.Truef(len(body) > 0, "thumbnail is empty")
75 | }
76 |
77 | func (s *ImageTestSuite) TestList() {
78 | ctx := context.Background()
79 | rsp1, err := s.srv.ListByDate(ctx, &pb.ListByDateRequest{})
80 | s.Nilf(err, "list failed: %v", err)
81 | s.Truef(rsp1.Success, "list failed: %s", rsp1.Message)
82 | s.Equal(0, len(rsp1.Paths))
83 | err = s.uploadPic1(ctx)
84 | s.Nil(err)
85 | s.Nil(waitfile(s.srv, pic1ShouldPath, 5*time.Second))
86 | rsp2, err := s.srv.ListByDate(ctx, &pb.ListByDateRequest{})
87 | s.Nilf(err, "list failed: %v", err)
88 | s.Truef(rsp2.Success, "list failed: %s", rsp2.Message)
89 | s.Equal(1, len(rsp2.Paths))
90 | s.Equalf(pic1ShouldPath, rsp2.Paths[0], "path: %s", rsp2.Paths[0])
91 | }
92 |
93 | func (s *ImageTestSuite) TestDelete() {
94 | ctx := context.Background()
95 | err := s.uploadPic1(ctx)
96 | s.Nil(err)
97 | s.Nil(waitfile(s.srv, pic1ShouldPath, 5*time.Second))
98 | rsp2, err := s.srv.ListByDate(ctx, &pb.ListByDateRequest{})
99 | s.Nilf(err, "list failed: %v", err)
100 | s.Truef(rsp2.Success, "list failed: %s", rsp2.Message)
101 | s.Equal(1, len(rsp2.Paths))
102 | rsp3, err := s.srv.Delete(ctx, &pb.DeleteRequest{
103 | Paths: []string{pic1ShouldPath},
104 | })
105 | s.Nilf(err, "delete failed: %v", err)
106 | s.Truef(rsp3.Success, "delete: %s", rsp3.Message)
107 | rsp4, err := s.srv.ListByDate(ctx, &pb.ListByDateRequest{})
108 | s.Nilf(err, "list failed: %v", err)
109 | s.Equal(0, len(rsp4.Paths))
110 | }
111 |
112 | func (s *ImageTestSuite) get(ctx context.Context, path string) ([]byte, error) {
113 | resp, err := http.Get(fmt.Sprintf("http://%s/%s", httpAddr, path))
114 | if err != nil {
115 | return nil, err
116 | }
117 | defer resp.Body.Close()
118 | if resp.StatusCode != http.StatusOK {
119 | return nil, fmt.Errorf("http status: %d", resp.StatusCode)
120 | }
121 | return io.ReadAll(resp.Body)
122 | }
123 |
124 | func (s *ImageTestSuite) uploadPic1(ctx context.Context) error {
125 | name := "pic1.jpg"
126 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/%s", httpAddr, name), bytes.NewReader(static.Pic1))
127 | req.Header.Set("Content-Type", "image/jpeg")
128 | req.Header.Set("Image-Date", "2022:11:08 12:34:36")
129 | resp, err := http.DefaultClient.Do(req)
130 | if err != nil {
131 | return err
132 | }
133 | defer resp.Body.Close()
134 | if resp.StatusCode != http.StatusOK {
135 | return fmt.Errorf("http status: %d", resp.StatusCode)
136 | }
137 | io.Copy(io.Discard, resp.Body)
138 |
139 | req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/thumbnail/%s", httpAddr, name), bytes.NewReader(static.Pic1))
140 | req.Header.Set("Content-Type", "image/jpeg")
141 | req.Header.Set("Image-Date", "2022:11:08 12:34:36")
142 | resp, err = http.DefaultClient.Do(req)
143 | if err != nil {
144 | return err
145 | }
146 | defer resp.Body.Close()
147 | if resp.StatusCode != http.StatusOK {
148 | return fmt.Errorf("http status: %d", resp.StatusCode)
149 | }
150 | io.Copy(io.Discard, resp.Body)
151 | return nil
152 | }
153 |
--------------------------------------------------------------------------------
/server/api/nfs.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | pb "github.com/fregie/img_syncer/proto"
8 | "github.com/fregie/img_syncer/server/drive/nfs"
9 | )
10 |
11 | func (a *api) SetDriveNFS(ctx context.Context, req *pb.SetDriveNFSRequest) (rsp *pb.SetDriveNFSResponse, e error) {
12 | rsp = &pb.SetDriveNFSResponse{Success: true}
13 | if req.Addr == "" {
14 | rsp.Success, rsp.Message = false, "param error: url is empty"
15 | return
16 | }
17 | d, err := nfs.NewNfsDrive(req.Addr)
18 | if err != nil {
19 | rsp.Success, rsp.Message = false, fmt.Sprintf("new nfs drive failed: %s", err.Error())
20 | return
21 | }
22 | a.im.SetDrive(d)
23 | if req.Root != "" {
24 | err := d.SetRootPath(req.Root)
25 | if err != nil {
26 | rsp.Success, rsp.Message = false, fmt.Sprintf("set root path failed: %s", err.Error())
27 | return
28 | }
29 | }
30 | return
31 | }
32 |
33 | func (a *api) ListDriveNFSDir(ctx context.Context, req *pb.ListDriveNFSDirRequest) (rsp *pb.ListDriveNFSDirResponse, e error) {
34 | rsp = &pb.ListDriveNFSDirResponse{Success: true}
35 | dri := a.im.Drive()
36 | if dri == nil {
37 | rsp.Success, rsp.Message = false, "drive is not set"
38 | return
39 | }
40 | nfs, ok := dri.(*nfs.Nfs)
41 | if !ok {
42 | rsp.Success, rsp.Message = false, "drive is not nfs"
43 | return
44 | }
45 | if req.Dir == "" {
46 | req.Dir = "/"
47 | }
48 | rsp.Dirs = make([]string, 0)
49 | cli := nfs.Cli()
50 | if cli == nil {
51 | rsp.Success, rsp.Message = false, "nfs client is not set"
52 | return
53 | }
54 | infos, err := cli.ReadDirPlus(req.Dir)
55 | if err != nil {
56 | rsp.Success, rsp.Message = false, fmt.Sprintf("list dir failed: %s", err.Error())
57 | return
58 | }
59 | for _, info := range infos {
60 | if info.IsDir() {
61 | rsp.Dirs = append(rsp.Dirs, info.Name())
62 | }
63 | }
64 | return
65 | }
66 |
--------------------------------------------------------------------------------
/server/api/smb.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "strings"
8 |
9 | pb "github.com/fregie/img_syncer/proto"
10 | "github.com/fregie/img_syncer/server/drive/smb"
11 | )
12 |
13 | func (a *api) SetDriveSMB(ctx context.Context, req *pb.SetDriveSMBRequest) (rsp *pb.SetDriveSMBResponse, err error) {
14 | rsp = &pb.SetDriveSMBResponse{Success: true}
15 | if req.Addr == "" {
16 | rsp.Success, rsp.Message = false, "param error: addr is empty"
17 | return
18 | }
19 | if strings.Index(req.Addr, ":") < 0 {
20 | req.Addr = req.Addr + ":445"
21 | }
22 | _, e := net.Dial("tcp", req.Addr)
23 | if err != nil {
24 | rsp.Success, rsp.Message = false, fmt.Sprintf("connect to %s failed: %s", req.Addr, e.Error())
25 | return
26 | }
27 | s := smb.NewSmbDrive(req.Addr, req.Username, req.Password)
28 | a.im.SetDrive(s)
29 | if req.Share != "" {
30 | e := s.SetShare(req.Share)
31 | if e != nil {
32 | rsp.Success, rsp.Message = false, fmt.Sprintf("set share failed: %s", e.Error())
33 | return
34 | }
35 | if req.Root != "" {
36 | e := s.SetRootPath(req.Root)
37 | if e != nil {
38 | rsp.Success, rsp.Message = false, fmt.Sprintf("set root path failed: %s", e.Error())
39 | return
40 | }
41 | }
42 | }
43 | return
44 | }
45 |
46 | func (a *api) ListDriveSMBShares(ctx context.Context, req *pb.ListDriveSMBSharesRequest) (rsp *pb.ListDriveSMBSharesResponse, err error) {
47 | rsp = &pb.ListDriveSMBSharesResponse{Success: true}
48 | dri := a.im.Drive()
49 | if dri == nil {
50 | rsp.Success, rsp.Message = false, "drive is not set"
51 | return
52 | }
53 | smb, ok := dri.(*smb.Smb)
54 | if !ok {
55 | rsp.Success, rsp.Message = false, "drive is not smb"
56 | return
57 | }
58 | shares, e := smb.ListShare()
59 | if e != nil {
60 | rsp.Success, rsp.Message = false, fmt.Sprintf("list share failed: %s", e.Error())
61 | return
62 | }
63 | rsp.Shares = shares
64 |
65 | return
66 | }
67 |
68 | func (a *api) ListDriveSMBDir(ctx context.Context, req *pb.ListDriveSMBDirRequest) (rsp *pb.ListDriveSMBDirResponse, err error) {
69 | rsp = &pb.ListDriveSMBDirResponse{Success: true}
70 | dri := a.im.Drive()
71 | if dri == nil {
72 | rsp.Success, rsp.Message = false, "drive is not set"
73 | return
74 | }
75 | smb, ok := dri.(*smb.Smb)
76 | if !ok {
77 | rsp.Success, rsp.Message = false, "drive is not smb"
78 | return
79 | }
80 | if req.Dir == "" {
81 | req.Dir = "."
82 | }
83 | sess, e := smb.Dial()
84 | if e != nil {
85 | rsp.Success, rsp.Message = false, fmt.Sprintf("dial failed: %s", e.Error())
86 | return
87 | }
88 | defer sess.Logoff()
89 | share, e := sess.Mount(req.Share)
90 | if e != nil {
91 | rsp.Success, rsp.Message = false, fmt.Sprintf("mount share failed: %s", e.Error())
92 | return
93 | }
94 | defer share.Umount()
95 | infos, e := share.ReadDir(req.Dir)
96 | if e != nil {
97 | rsp.Success, rsp.Message = false, fmt.Sprintf("read dir failed: %s", e.Error())
98 | return
99 | }
100 | rsp.Dirs = make([]string, 0)
101 | for _, info := range infos {
102 | if info.IsDir() {
103 | rsp.Dirs = append(rsp.Dirs, info.Name())
104 | }
105 | }
106 |
107 | return
108 | }
109 |
110 | func (a *api) SetDriveSMBShare(ctx context.Context, req *pb.SetDriveSMBShareRequest) (rsp *pb.SetDriveSMBShareResponse, err error) {
111 | rsp = &pb.SetDriveSMBShareResponse{Success: true}
112 | dri := a.im.Drive()
113 | if dri == nil {
114 | rsp.Success, rsp.Message = false, "drive is not set"
115 | return
116 | }
117 | smb, ok := dri.(*smb.Smb)
118 | if !ok {
119 | rsp.Success, rsp.Message = false, "drive is not smb"
120 | return
121 | }
122 | e := smb.SetShare(req.Share)
123 | if e != nil {
124 | rsp.Success, rsp.Message = false, fmt.Sprintf("set share failed: %s", e.Error())
125 | return
126 | }
127 | e = smb.SetRootPath(req.Root)
128 | if e != nil {
129 | rsp.Success, rsp.Message = false, fmt.Sprintf("set root path failed: %s", e.Error())
130 | return
131 | }
132 | return
133 | }
134 |
--------------------------------------------------------------------------------
/server/api/smb_test.go:
--------------------------------------------------------------------------------
1 | package api_test
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 | "testing"
8 | "time"
9 |
10 | pb "github.com/fregie/img_syncer/proto"
11 | "github.com/fregie/img_syncer/test/static"
12 | "github.com/hirochachacha/go-smb2"
13 | "github.com/stretchr/testify/suite"
14 | "golang.org/x/net/context"
15 | "google.golang.org/grpc"
16 | )
17 |
18 | type DriveTestSuite struct {
19 | suite.Suite
20 | srv pb.ImgSyncerClient
21 | share *smb2.Share
22 | }
23 |
24 | func TestDriveTestSuite(t *testing.T) {
25 | suite.Run(t, new(DriveTestSuite))
26 | }
27 |
28 | func (s *DriveTestSuite) SetupTest() {
29 | err := cleanSmb()
30 | s.Nilf(err, "failed to clean smb share: %s", err)
31 | err = initSmbDir()
32 | s.Nilf(err, "failed to init smb dir: %s", err)
33 | grpcConn, err := grpc.Dial(grpcAddr, grpc.WithInsecure())
34 | s.Nil(err)
35 | s.srv = pb.NewImgSyncerClient(grpcConn)
36 | s.share, err = initSmbShare()
37 | s.Nil(err)
38 | }
39 |
40 | func (s *DriveTestSuite) TestSetDriveSMB() {
41 | ctx := context.Background()
42 | // test set drive smb
43 | rsp1, err := s.srv.SetDriveSMB(ctx, &pb.SetDriveSMBRequest{
44 | Addr: smbSrvAddr,
45 | Username: smbUser,
46 | Password: smbPass,
47 | })
48 | s.Nil(err)
49 | s.True(rsp1.Success)
50 | // test list drive smb shares
51 | rsp2, err := s.srv.ListDriveSMBShares(ctx, &pb.ListDriveSMBSharesRequest{})
52 | s.Nil(err)
53 | s.Truef(rsp2.Success, "failed to list smb shares: %s", rsp2.Message)
54 | s.Containsf(rsp2.Shares, smbShare, "shares %v not contains %s", rsp2.Shares, smbShare)
55 | rsp2_1, err := s.srv.ListDriveSMBDir(ctx, &pb.ListDriveSMBDirRequest{
56 | Share: smbShare,
57 | Dir: ".",
58 | })
59 | s.Nil(err)
60 | s.Truef(rsp2_1.Success, "failed to list smb dir: %s", rsp2_1.Message)
61 | s.Containsf(rsp2_1.Dirs, smbRootDir, "files %v not contains %s", rsp2_1.Dirs, smbRootDir)
62 | // test set drive smb share
63 | rsp3, err := s.srv.SetDriveSMBShare(ctx, &pb.SetDriveSMBShareRequest{
64 | Share: smbShare,
65 | Root: smbRootDir,
66 | })
67 | s.Nil(err)
68 | s.Truef(rsp3.Success, "failed to set smb share: %s", rsp3.Message)
69 | // test upload
70 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/pic1.jpg", httpAddr), bytes.NewReader(static.Pic1))
71 | s.Nilf(err, "new request failed: %v", err)
72 | req.Header.Set("Content-Type", "image/jpeg")
73 | req.Header.Set("Image-Date", "2022:11:08 12:34:36")
74 | resp, err := http.DefaultClient.Do(req)
75 | s.Nilf(err, "upload pic failed: %v", err)
76 | s.Equal(http.StatusOK, resp.StatusCode)
77 |
78 | filePath := "storage/2022/11/08/20221108123436_pic1.jpg"
79 | s.waitFile(filePath, 5*time.Second)
80 | fdata, err := s.share.ReadFile(filePath)
81 | s.Nilf(err, "failed to read file: %s", err)
82 | s.Equal(static.Pic1, fdata)
83 | }
84 |
85 | func (s *DriveTestSuite) waitFile(path string, timeout time.Duration) {
86 | start := time.Now()
87 | for {
88 | _, err := s.share.Stat(path)
89 | if err == nil {
90 | break
91 | }
92 | if time.Since(start) > timeout {
93 | s.FailNowf("wait file timeout", "wait file %s timeout", path)
94 | }
95 | time.Sleep(200 * time.Millisecond)
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/server/api/util_test.go:
--------------------------------------------------------------------------------
1 | package api_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "net"
8 | "net/http"
9 | "os"
10 | "time"
11 |
12 | pb "github.com/fregie/img_syncer/proto"
13 | "github.com/hirochachacha/go-smb2"
14 | )
15 |
16 | const (
17 | grpcAddr = "127.0.0.1:50051"
18 | httpAddr = "127.0.0.1:8000"
19 | smbSrvAddr = "smb"
20 | smbAddr = "127.0.0.1:445"
21 | smbUser = "fregie"
22 | smbPass = "password"
23 | smbShare = "photos"
24 | smbRootDir = "storage"
25 |
26 | pic1ShouldPath = "2022/11/08/20221108123436_pic1.jpg"
27 | )
28 |
29 | func initSmbShare() (*smb2.Share, error) {
30 | conn, err := net.Dial("tcp", smbAddr)
31 | if err != nil {
32 | return nil, err
33 | }
34 | d := &smb2.Dialer{
35 | Initiator: &smb2.NTLMInitiator{
36 | User: smbUser,
37 | Password: smbPass,
38 | },
39 | }
40 | s, err := d.Dial(conn)
41 | if err != nil {
42 | return nil, err
43 | }
44 | share, err := s.Mount(smbShare)
45 | if err != nil {
46 | return nil, err
47 | }
48 | return share, nil
49 | }
50 |
51 | func getSmbShare() (*smb2.Share, error) {
52 | conn, err := net.Dial("tcp", smbAddr)
53 | if err != nil {
54 | return nil, err
55 | }
56 | d := &smb2.Dialer{
57 | Initiator: &smb2.NTLMInitiator{
58 | User: smbUser,
59 | Password: smbPass,
60 | },
61 | }
62 | s, err := d.Dial(conn)
63 | if err != nil {
64 | return nil, err
65 | }
66 | share, err := s.Mount(smbShare)
67 | if err != nil {
68 | return nil, err
69 | }
70 | return share, nil
71 | }
72 |
73 | func cleanSmb() error {
74 | share, err := getSmbShare()
75 | if err != nil {
76 | return err
77 | }
78 | retriedTimes := 0
79 | Retry:
80 | dirs, err := share.ReadDir(".")
81 | if err != nil {
82 | return err
83 | }
84 | for _, dir := range dirs {
85 | if dir.IsDir() {
86 | if err := share.RemoveAll(dir.Name()); err != nil {
87 | if retriedTimes <= 3 {
88 | time.Sleep(300 * time.Microsecond)
89 | retriedTimes++
90 | goto Retry
91 | }
92 | fmt.Printf("remove %s error: %v\n", dir.Name(), err)
93 | continue
94 | }
95 | } else {
96 | if err := share.Remove(dir.Name()); err != nil {
97 | if retriedTimes <= 3 {
98 | time.Sleep(300 * time.Microsecond)
99 | retriedTimes++
100 | goto Retry
101 | }
102 | fmt.Printf("remove %s error: %v\n", dir.Name(), err)
103 | continue
104 | }
105 | }
106 | }
107 | return nil
108 | }
109 |
110 | func initSmbDir() error {
111 | share, err := getSmbShare()
112 | if err != nil {
113 | return err
114 | }
115 | if err := share.Mkdir(smbRootDir, os.ModePerm); err != nil {
116 | return err
117 | }
118 | return nil
119 | }
120 |
121 | func waitfile(srv pb.ImgSyncerClient, path string, timeout time.Duration) error {
122 | ctx, cancel := context.WithTimeout(context.Background(), timeout)
123 | if path[0] != '/' {
124 | path = "/" + path
125 | }
126 | defer cancel()
127 | for {
128 | select {
129 | case <-ctx.Done():
130 | return ctx.Err()
131 | default:
132 | resp, err := http.Get(fmt.Sprintf("http://%s%s", httpAddr, path))
133 | if err != nil {
134 | goto CONTINUE
135 | }
136 | defer resp.Body.Close()
137 | data, err := io.ReadAll(resp.Body)
138 | if err != nil {
139 | goto CONTINUE
140 | }
141 | rsp := string(data)
142 | rsp = rsp
143 | if resp.StatusCode != http.StatusOK {
144 | goto CONTINUE
145 | }
146 | return nil
147 | }
148 | CONTINUE:
149 | time.Sleep(200 * time.Millisecond)
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/server/api/webdav.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | pb "github.com/fregie/img_syncer/proto"
8 | "github.com/fregie/img_syncer/server/drive/webdav"
9 | )
10 |
11 | func (a *api) SetDriveWebdav(ctx context.Context, req *pb.SetDriveWebdavRequest) (rsp *pb.SetDriveWebdavResponse, e error) {
12 | rsp = &pb.SetDriveWebdavResponse{Success: true}
13 | if req.Addr == "" {
14 | rsp.Success, rsp.Message = false, "param error: url is empty"
15 | return
16 | }
17 | d := webdav.NewWebdavDrive(req.Addr, req.Username, req.Password)
18 | a.im.SetDrive(d)
19 | if req.Root != "" {
20 | err := d.SetRootPath(req.Root)
21 | if err != nil {
22 | rsp.Success, rsp.Message = false, fmt.Sprintf("set root path failed: %s", err.Error())
23 | return
24 | }
25 | }
26 | return
27 | }
28 |
29 | func (a *api) ListDriveWebdavDir(ctx context.Context, req *pb.ListDriveWebdavDirRequest) (rsp *pb.ListDriveWebdavDirResponse, e error) {
30 | rsp = &pb.ListDriveWebdavDirResponse{Success: true}
31 | dri := a.im.Drive()
32 | if dri == nil {
33 | rsp.Success, rsp.Message = false, "drive is not set"
34 | return
35 | }
36 | webdav, ok := dri.(*webdav.Webdav)
37 | if !ok {
38 | rsp.Success, rsp.Message = false, "drive is not webdav"
39 | return
40 | }
41 | if req.Dir == "" {
42 | req.Dir = "/"
43 | }
44 | rsp.Dirs = make([]string, 0)
45 | cli := webdav.Cli()
46 | if cli == nil {
47 | rsp.Success, rsp.Message = false, "webdav client is not set"
48 | return
49 | }
50 | infos, err := cli.ReadDir(req.Dir)
51 | if err != nil {
52 | rsp.Success, rsp.Message = false, fmt.Sprintf("list dir failed: %s", err.Error())
53 | return
54 | }
55 | for _, info := range infos {
56 | if info.IsDir() {
57 | rsp.Dirs = append(rsp.Dirs, info.Name())
58 | }
59 | }
60 |
61 | return
62 | }
63 |
--------------------------------------------------------------------------------
/server/api/webdav_test.go:
--------------------------------------------------------------------------------
1 | package api_test
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "os"
10 | "path/filepath"
11 | "testing"
12 | "time"
13 |
14 | pb "github.com/fregie/img_syncer/proto"
15 | "github.com/fregie/img_syncer/test/static"
16 | "github.com/stretchr/testify/suite"
17 | "github.com/studio-b12/gowebdav"
18 | "google.golang.org/grpc"
19 | )
20 |
21 | const (
22 | webdavUrl = "http://127.0.0.1:8080"
23 | webdavSrvAddr = "http://webdav"
24 | webdavUser = "fregie"
25 | webdavPass = "password"
26 | webdavRootPath = "storage"
27 | )
28 |
29 | type DriveWebdavTestSuite struct {
30 | suite.Suite
31 | srv pb.ImgSyncerClient
32 | cli *gowebdav.Client
33 | }
34 |
35 | func TestDriveWebdavTestSuite(t *testing.T) {
36 | suite.Run(t, new(DriveWebdavTestSuite))
37 | }
38 |
39 | func (s *DriveWebdavTestSuite) SetupTest() {
40 | err := cleanWebdav()
41 | s.Nilf(err, "failed to clean webdav: %s", err)
42 | err = initWebdavDir()
43 | s.Nilf(err, "failed to init webdav dir: %s", err)
44 | grpcConn, err := grpc.Dial(grpcAddr, grpc.WithInsecure())
45 | s.Nil(err)
46 | s.srv = pb.NewImgSyncerClient(grpcConn)
47 | s.cli = gowebdav.NewClient(webdavUrl, webdavUser, webdavPass)
48 | }
49 |
50 | // TestSetDriveWebdav tests set drive webdav
51 | func (s *DriveWebdavTestSuite) TestSetDriveWebdav() {
52 | ctx := context.Background()
53 | // test set drive webdav
54 | rsp1, err := s.srv.SetDriveWebdav(ctx, &pb.SetDriveWebdavRequest{
55 | Addr: webdavSrvAddr,
56 | Username: webdavUser,
57 | Password: webdavPass,
58 | })
59 | s.Nilf(err, "set drive webdav failed: %v", err)
60 | s.True(rsp1.Success)
61 | // test list drive webdav dirs
62 | rsp2, err := s.srv.ListDriveWebdavDir(ctx, &pb.ListDriveWebdavDirRequest{})
63 | s.Nil(err)
64 | s.True(rsp2.Success)
65 | s.Containsf(rsp2.Dirs, webdavRootPath, "webdav root path not found")
66 | // test set drive webdav with root path
67 | rsp3, err := s.srv.SetDriveWebdav(ctx, &pb.SetDriveWebdavRequest{
68 | Addr: webdavSrvAddr,
69 | Username: webdavUser,
70 | Password: webdavPass,
71 | Root: webdavRootPath,
72 | })
73 | s.Nil(err)
74 | s.Truef(rsp3.Success, "failed to set drive webdav with root path: %s", rsp3.Message)
75 | }
76 |
77 | // test upload
78 | func (s *DriveWebdavTestSuite) TestUploadDownload() {
79 | ctx := context.Background()
80 | // test set drive webdav with root path
81 | rsp3, err := s.srv.SetDriveWebdav(ctx, &pb.SetDriveWebdavRequest{
82 | Addr: webdavSrvAddr,
83 | Username: webdavUser,
84 | Password: webdavPass,
85 | Root: webdavRootPath,
86 | })
87 | s.Nil(err)
88 | s.True(rsp3.Success)
89 | // test upload
90 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/pic1.jpg", httpAddr), bytes.NewReader(static.Pic1))
91 | s.Nilf(err, "new request failed: %v", err)
92 | req.Header.Set("Content-Type", "image/jpeg")
93 | req.Header.Set("Image-Date", "2022:11:08 12:34:36")
94 | resp, err := http.DefaultClient.Do(req)
95 | s.Nilf(err, "upload pic failed: %v", err)
96 | s.Equal(http.StatusOK, resp.StatusCode)
97 | filePath := "/storage/2022/11/08/20221108123436_pic1.jpg"
98 | s.waitFile(filePath, 5*time.Second)
99 | fdata, err := s.cli.Read(filePath)
100 | s.Nil(err)
101 | s.Equal(len(fdata), len(static.Pic1))
102 | // s.Equalf(fdata, static.Pic1, "file data not equal")
103 |
104 | // test download
105 | data, err := s.get(ctx, pic1ShouldPath)
106 | s.Nil(err)
107 | s.Equal(len(data), len(static.Pic1))
108 | // s.Equalf(data, static.Pic1, "file data not equal")
109 | }
110 |
111 | // waitFile waits for file to be ready
112 | func (s *DriveWebdavTestSuite) waitFile(path string, timeout time.Duration) {
113 | if path == "" {
114 | s.FailNow("path is empty")
115 | }
116 | path = filepath.ToSlash(path)
117 | start := time.Now()
118 | if path[0] != '/' {
119 | path = "/" + path
120 | }
121 | for {
122 | _, err := s.cli.Stat(path)
123 | if err == nil {
124 | break
125 | }
126 | if time.Since(start) > timeout {
127 | s.FailNowf("wait file timeout", "wait file %s timeout", path)
128 | }
129 | time.Sleep(200 * time.Millisecond)
130 | }
131 | }
132 |
133 | func (s *DriveWebdavTestSuite) get(ctx context.Context, path string) ([]byte, error) {
134 | if path[0] != '/' {
135 | path = "/" + path
136 | }
137 | resp, err := http.Get(fmt.Sprintf("http://%s%s", httpAddr, path))
138 | if err != nil {
139 | return nil, err
140 | }
141 | defer resp.Body.Close()
142 | if resp.StatusCode != http.StatusOK {
143 | return nil, fmt.Errorf(resp.Status)
144 | }
145 | return io.ReadAll(resp.Body)
146 | }
147 |
148 | func cleanWebdav() error {
149 | cli := gowebdav.NewClient(webdavUrl, webdavUser, webdavPass)
150 | dirs, err := cli.ReadDir("/")
151 | if err != nil {
152 | return err
153 | }
154 | for _, dir := range dirs {
155 | err = cli.RemoveAll("/" + dir.Name() + "/")
156 | if err != nil {
157 | return err
158 | }
159 | }
160 | return nil
161 | }
162 |
163 | func initWebdavDir() error {
164 | cli := gowebdav.NewClient(webdavUrl, webdavUser, webdavPass)
165 | if err := cli.Mkdir(webdavRootPath, os.ModePerm); err != nil {
166 | return err
167 | }
168 | return nil
169 | }
170 |
--------------------------------------------------------------------------------
/server/drive/baidu/errorno.go:
--------------------------------------------------------------------------------
1 | package baidu
2 |
3 | const (
4 | ErrorNoSuccess = 0
5 | ErrorNoAccessToken = 111
6 | ErrorNoFileNotExist = -31066
7 | ErrorNoFileNotExist2 = -3
8 | ErrorNoFileNotExist3 = -9
9 | ErrorAlreadyExists = -8
10 | )
11 |
--------------------------------------------------------------------------------
/server/drive/baidu/token.go:
--------------------------------------------------------------------------------
1 | package baidu
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "log"
8 | "net/http"
9 | "time"
10 | )
11 |
12 | type tokenResp struct {
13 | AccessToken string `json:"access_token"`
14 | RefreshToken string `json:"refresh_token"`
15 | ExipresIn int `json:"expires_in"`
16 | }
17 |
18 | func (d *BaiduNetdisk) RefreshToken() string {
19 | d.tokenLock.RLock()
20 | defer d.tokenLock.RUnlock()
21 | return d.refreshToken
22 | }
23 |
24 | func (d *BaiduNetdisk) AccessToken() string {
25 | d.tokenLock.RLock()
26 | defer d.tokenLock.RUnlock()
27 | return d.accessToken
28 | }
29 |
30 | func (d *BaiduNetdisk) isTokenAvaliable() bool {
31 | d.tokenLock.RLock()
32 | defer d.tokenLock.RUnlock()
33 | if d.accessToken == "" {
34 | return false
35 | }
36 | if d.TokenExpireAt > 0 && d.TokenExpireAt < time.Now().Unix() {
37 | return false
38 | }
39 | return true
40 | }
41 |
42 | func (d *BaiduNetdisk) refreshAccessToken() error {
43 | if d.refreshToken == "" {
44 | return fmt.Errorf("refresh token is empty")
45 | }
46 | url := fmt.Sprintf("https://openapi.baidu.com/oauth/2.0/token?grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s", d.RefreshToken(), PhoAppKey, PhoSecretKey)
47 | req, err := http.NewRequest("GET", url, nil)
48 | if err != nil {
49 | return err
50 | }
51 | resp, err := d.httpc.Do(req)
52 | if err != nil {
53 | return fmt.Errorf("refresh token failed: %v", err)
54 | }
55 | defer resp.Body.Close()
56 | data, _ := io.ReadAll(resp.Body)
57 | log.Printf("refresh token resp: %s", string(data))
58 | var token tokenResp
59 | if err := json.Unmarshal(data, &token); err != nil {
60 | return fmt.Errorf("refresh token failed: %v", err)
61 | }
62 | d.tokenLock.Lock()
63 | d.accessToken = token.AccessToken
64 | d.refreshToken = token.RefreshToken
65 | d.TokenExpireAt = time.Now().Unix() + int64(token.ExipresIn)
66 | d.tokenLock.Unlock()
67 | return nil
68 | }
69 |
--------------------------------------------------------------------------------
/server/drive/webdav/webdav.go:
--------------------------------------------------------------------------------
1 | package webdav
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "io"
7 | "io/fs"
8 | "net/http"
9 | "os"
10 | "path/filepath"
11 | "sort"
12 | "time"
13 |
14 | "github.com/studio-b12/gowebdav"
15 | )
16 |
17 | type Webdav struct {
18 | url string
19 | username string
20 | password string
21 | rootPath string
22 | cli *gowebdav.Client
23 | }
24 |
25 | func NewWebdavDrive(url, username, password string) *Webdav {
26 | d := &Webdav{
27 | url: url,
28 | username: username,
29 | password: password,
30 | cli: gowebdav.NewClient(url, username, password),
31 | }
32 | d.cli.SetTransport(&http.Transport{
33 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
34 | })
35 | return d
36 | }
37 |
38 | func (d *Webdav) Cli() *gowebdav.Client {
39 | return d.cli
40 | }
41 |
42 | func (d *Webdav) IsRootPathSet() bool {
43 | return d.rootPath != ""
44 | }
45 |
46 | func (d *Webdav) SetRootPath(rootPath string) error {
47 | if rootPath == "" {
48 | return fmt.Errorf("root path is empty")
49 | }
50 | rootPath = filepath.ToSlash(rootPath)
51 | var err error
52 | if rootPath[0] != '/' {
53 | rootPath = "/" + rootPath
54 | }
55 | if rootPath[len(rootPath)-1] != '/' {
56 | rootPath = rootPath + "/"
57 | }
58 | info, err := d.cli.Stat(rootPath)
59 | if err != nil {
60 | if os.IsNotExist(err) {
61 | return fmt.Errorf("root path %s not exist", rootPath)
62 | }
63 | return err
64 | }
65 | if !info.IsDir() {
66 | return fmt.Errorf("root path %s is not a dir", rootPath)
67 | }
68 | d.rootPath = rootPath
69 | return nil
70 | }
71 |
72 | func (d *Webdav) IsExist(path string) (bool, error) {
73 | if d.rootPath == "" {
74 | return false, fmt.Errorf("root path is empty")
75 | }
76 | fullPath := filepath.Join(d.rootPath, path)
77 | _, err := d.cli.Stat(fullPath)
78 | if err != nil {
79 | if os.IsNotExist(err) {
80 | return false, nil
81 | }
82 | if pathErr, ok := err.(*os.PathError); ok {
83 | if statusErr, ok := pathErr.Err.(gowebdav.StatusError); ok && statusErr.Status == 404 {
84 | return false, nil
85 | }
86 | }
87 | return false, err
88 | }
89 | return true, nil
90 | }
91 |
92 | func (d *Webdav) Download(path string) (io.ReadCloser, int64, error) {
93 | if d.rootPath == "" {
94 | return nil, 0, fmt.Errorf("root path is empty")
95 | }
96 | fullPath := filepath.Join(d.rootPath, path)
97 | reader, err := d.cli.ReadStream(fullPath)
98 | if err != nil {
99 | return nil, 0, err
100 | }
101 | info, err := d.cli.Stat(fullPath)
102 | if err != nil {
103 | return nil, 0, err
104 | }
105 | return reader, info.Size(), nil
106 | // data, err := d.cli.Read(fullPath)
107 | // if err != nil {
108 | // return nil, 0, err
109 | // }
110 | // reader := io.NopCloser(bytes.NewReader(data))
111 | // return reader, int64(len(data)), nil
112 | }
113 |
114 | func (d *Webdav) Delete(path string) error {
115 | if d.rootPath == "" {
116 | return fmt.Errorf("root path is empty")
117 | }
118 | fullPath := filepath.Join(d.rootPath, path)
119 | err := d.cli.Remove(fullPath)
120 | if err != nil {
121 | return err
122 | }
123 | return nil
124 | }
125 |
126 | func (d *Webdav) DownloadWithOffset(path string, offset int64) (io.ReadCloser, int64, error) {
127 | if d.rootPath == "" {
128 | return nil, 0, fmt.Errorf("root path is empty")
129 | }
130 | fullPath := filepath.Join(d.rootPath, path)
131 | reader, err := d.cli.ReadStreamRange(fullPath, offset, -1)
132 | if err != nil {
133 | return nil, 0, err
134 | }
135 | info, err := d.cli.Stat(fullPath)
136 | if err != nil {
137 | return nil, 0, err
138 | }
139 |
140 | return reader, info.Size(), nil
141 | }
142 |
143 | func (d *Webdav) Upload(path string, reader io.ReadCloser, size int64, lastModified time.Time) error {
144 | if reader == nil {
145 | return fmt.Errorf("reader is nil")
146 | }
147 | defer reader.Close()
148 | if d.rootPath == "" {
149 | return fmt.Errorf("root path is empty")
150 | }
151 | fullPath := filepath.Join(d.rootPath, path)
152 | err := d.cli.MkdirAll(filepath.Dir(fullPath), 0755)
153 | if err != nil {
154 | return err
155 | }
156 | err = d.cli.WriteStream(fullPath, reader, 0666)
157 | if err != nil {
158 | return err
159 | }
160 |
161 | return nil
162 | }
163 |
164 | func (d *Webdav) Range(dir string, deal func(fs.FileInfo) bool) error {
165 | if d.rootPath == "" {
166 | return fmt.Errorf("root path is empty")
167 | }
168 | fullPath := filepath.Join(d.rootPath, dir)
169 | infos, err := d.cli.ReadDir(fullPath)
170 | if err != nil {
171 | return err
172 | }
173 | sort.Sort(desc(infos))
174 | for _, info := range infos {
175 | if !deal(info) {
176 | break
177 | }
178 | }
179 | return nil
180 | }
181 |
182 | type desc []fs.FileInfo
183 |
184 | func (d desc) Len() int { return len(d) }
185 | func (d desc) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
186 | func (d desc) Less(i, j int) bool {
187 | return d[i].ModTime().After(d[j].ModTime())
188 | }
189 |
--------------------------------------------------------------------------------
/server/imgmanager/interface.go:
--------------------------------------------------------------------------------
1 | package imgmanager
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "io/fs"
7 | "time"
8 | )
9 |
10 | type StorageDrive interface {
11 | Upload(string, io.ReadCloser, int64, time.Time) error
12 | // IsExist(path string) (bool, error)
13 | Download(path string) (io.ReadCloser, int64, error)
14 | DownloadWithOffset(path string, offset int64) (io.ReadCloser, int64, error)
15 | Delete(path string) error
16 | Range(dir string, deal func(fs.FileInfo) bool) error
17 | }
18 |
19 | type Image struct {
20 | Content io.ReadCloser
21 | Path string
22 | Size int64
23 | ImageMetadata
24 | }
25 |
26 | const (
27 | // ContentTypeJpeg = "image/jpeg"
28 | // ContentTypePng = "image/png"
29 | // ContentTypeGif = "image/gif"
30 |
31 | JpegSuffix = ".jpg"
32 | PngSuffix = ".png"
33 | DngSuffix = ".dng"
34 | )
35 |
36 | type UnimplementedDrive struct{}
37 |
38 | func (d *UnimplementedDrive) Upload(_ string, _ io.ReadCloser, _ int64, _ time.Time) error {
39 | return errors.New("no available drive")
40 | }
41 |
42 | func (d *UnimplementedDrive) IsExist(path string) (bool, error) {
43 | return false, errors.New("no available drive")
44 | }
45 |
46 | func (d *UnimplementedDrive) Download(path string) (io.ReadCloser, int64, error) {
47 | return nil, 0, errors.New("no available drive")
48 | }
49 |
50 | func (d *UnimplementedDrive) DownloadWithOffset(path string, offset int64) (io.ReadCloser, int64, error) {
51 | return nil, 0, errors.New("no available drive")
52 | }
53 |
54 | func (d *UnimplementedDrive) Delete(path string) error {
55 | return errors.New("no available drive")
56 | }
57 |
58 | func (d *UnimplementedDrive) Range(dir string, deal func(fs.FileInfo) bool) error {
59 | return errors.New("no available drive")
60 | }
61 |
--------------------------------------------------------------------------------
/server/imgmanager/metadata.go:
--------------------------------------------------------------------------------
1 | package imgmanager
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/dsoprea/go-exif/v3"
7 | exifcommon "github.com/dsoprea/go-exif/v3/common"
8 | )
9 |
10 | type ImageMetadata struct {
11 | Model string
12 | Datetime string
13 | CreateDate string
14 | DateTimeOriginal string
15 | ModifyDate string
16 | }
17 |
18 | func GetImageMetadata(content []byte) (ImageMetadata, error) {
19 | rawExif, err := exif.SearchAndExtractExif(content)
20 | if err != nil {
21 | return ImageMetadata{}, err
22 | }
23 | ifdMapping, err := exifcommon.NewIfdMappingWithStandard()
24 | if err != nil {
25 | return ImageMetadata{}, err
26 | }
27 | ti := exif.NewTagIndex()
28 | _, index, err := exif.Collect(ifdMapping, ti, rawExif)
29 | if err != nil {
30 | return ImageMetadata{}, err
31 | }
32 | rootIfd := index.RootIfd
33 | // for _, v := range rootIfd.DumpTags() {
34 | // log.Println(v.TagName())
35 | // }
36 | im := ImageMetadata{}
37 | im.Model, _ = getExifValue(rootIfd, "Model")
38 | im.Datetime, _ = getExifValue(rootIfd, "DateTime")
39 | im.CreateDate, _ = getExifValue(rootIfd, "CreateDate")
40 | im.DateTimeOriginal, _ = getExifValue(rootIfd, "DateTimeOriginal")
41 | im.ModifyDate, _ = getExifValue(rootIfd, "ModifyDate")
42 |
43 | return im, nil
44 | }
45 |
46 | func getExifValue(rootIfd *exif.Ifd, name string) (string, error) {
47 | results, err := rootIfd.FindTagWithName(name)
48 | if err != nil {
49 | return "", err
50 | }
51 | if len(results) != 1 {
52 | return "", errors.New("there wasn't exactly one result of img metadata[" + name + "]")
53 | }
54 | value, err := results[0].Value()
55 | if err != nil {
56 | return "", err
57 | }
58 | var strValue string
59 | if str, ok := value.(string); ok {
60 | strValue = str
61 | }
62 | return strValue, nil
63 | }
64 |
--------------------------------------------------------------------------------
/server/log.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io/ioutil"
5 | "log"
6 | "os"
7 | )
8 |
9 | var (
10 | //Debug print debug informantion
11 | Debug *log.Logger
12 | //Info print Info informantion
13 | Info *log.Logger
14 | //Error print Error informantion
15 | Error *log.Logger
16 | )
17 |
18 | func init() {
19 | Info = log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime)
20 | Error = log.New(os.Stderr, "[ERROR] ", log.Ldate|log.Ltime|log.Lshortfile)
21 | Debug = log.New(ioutil.Discard, "[DEBUG] ", log.Ldate|log.Ltime|log.Lshortfile)
22 | }
23 |
--------------------------------------------------------------------------------
/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "net"
6 | "os"
7 |
8 | "net/http"
9 | _ "net/http/pprof"
10 |
11 | version "github.com/fregie/PrintVersion"
12 | pb "github.com/fregie/img_syncer/proto"
13 | "github.com/fregie/img_syncer/server/api"
14 | "github.com/fregie/img_syncer/server/imgmanager"
15 | "google.golang.org/grpc"
16 | "google.golang.org/grpc/reflection"
17 | )
18 |
19 | var (
20 | grpcAddr = flag.String("grpcAddr", "0.0.0.0:50051", "grpc addr example: 0.0.0.0:50051")
21 | httpAddr = flag.String("httpAddr", "0.0.0.0:8000", "http addr example: 0.0.0.0:8000")
22 | showVersion = flag.Bool("version", false, "Displays version and exit.")
23 | debug = flag.Bool("d", false, "debug mode")
24 | )
25 |
26 | var (
27 | imgManager *imgmanager.ImgManager
28 | )
29 |
30 | func main() {
31 | flag.Parse()
32 | if *showVersion {
33 | version.PrintVersion()
34 | return
35 | }
36 | if *debug {
37 | Debug.SetOutput(os.Stdout)
38 | Debug.Printf("pprof listen at 0.0.0.0:6060")
39 | go http.ListenAndServe("0.0.0.0:6060", nil)
40 | }
41 | imgManager = imgmanager.NewImgManager(imgmanager.Option{})
42 |
43 | lis, err := net.Listen("tcp", *grpcAddr)
44 | if err != nil {
45 | Error.Fatalf("failed to listen: %v", err)
46 | }
47 |
48 | apiServer := api.NewApi(imgManager)
49 | Info.Printf("Listening http on %s", *httpAddr)
50 | go http.ListenAndServe(*httpAddr, apiServer.HttpHandler())
51 |
52 | grpcServer := grpc.NewServer()
53 | pb.RegisterImgSyncerServer(grpcServer, apiServer)
54 | reflection.Register(grpcServer)
55 | Info.Printf("Listening grpc on %s", lis.Addr().String())
56 | Error.Fatal(grpcServer.Serve(lis))
57 | }
58 |
--------------------------------------------------------------------------------
/server/run/run.go:
--------------------------------------------------------------------------------
1 | package run
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "net"
8 | "net/http"
9 | "os"
10 |
11 | "github.com/fregie/img_syncer/server/api"
12 | "github.com/fregie/img_syncer/server/imgmanager"
13 | _ "golang.org/x/mobile/bind"
14 | "google.golang.org/grpc"
15 | "google.golang.org/grpc/reflection"
16 |
17 | pb "github.com/fregie/img_syncer/proto"
18 | )
19 |
20 | var (
21 | imgManager *imgmanager.ImgManager
22 | )
23 |
24 | func RunGrpcServer() (string, error) {
25 | imgManager = imgmanager.NewImgManager(imgmanager.Option{})
26 | var grpcLis, httpLis net.Listener
27 | var err error
28 | var grpcPort, httpPort int
29 | for start := 10000; start < 20000; start++ {
30 | grpcLis, err = net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", start))
31 | if err != nil {
32 | Info.Printf("Listen on %d failed, try next port", start)
33 | continue
34 | } else {
35 | grpcPort = start
36 | break
37 | }
38 | }
39 | if err != nil {
40 | Error.Printf("Listen on all port failed, err: %v", err)
41 | return "", err
42 | }
43 |
44 | for start := 10000; start < 20000; start++ {
45 | httpLis, err = net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", start))
46 | if err != nil {
47 | Info.Printf("Listen on %d failed, try next port", start)
48 | continue
49 | } else {
50 | httpPort = start
51 | break
52 | }
53 | }
54 | if err != nil {
55 | Error.Printf("Listen on all port failed, err: %v", err)
56 | return "", err
57 | }
58 |
59 | api := api.NewApi(imgManager)
60 | api.SetHttpPort(httpPort)
61 | grpcServer := grpc.NewServer()
62 | pb.RegisterImgSyncerServer(grpcServer, api)
63 | reflection.Register(grpcServer)
64 |
65 | Info.Printf("Listening grpc on %s", grpcLis.Addr().String())
66 | go grpcServer.Serve(grpcLis)
67 | Info.Printf("Listening http on %s", httpLis.Addr().String())
68 | go http.Serve(httpLis, api.HttpHandler())
69 |
70 | return fmt.Sprintf("%d,%d", grpcPort, httpPort), nil
71 | }
72 |
73 | var (
74 | //Debug print debug informantion
75 | Debug *log.Logger
76 | //Info print Info informantion
77 | Info *log.Logger
78 | //Error print Error informantion
79 | Error *log.Logger
80 | )
81 |
82 | func init() {
83 | Info = log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime)
84 | Error = log.New(os.Stderr, "[ERROR] ", log.Ldate|log.Ltime|log.Lshortfile)
85 | Debug = log.New(ioutil.Discard, "[DEBUG] ", log.Ldate|log.Ltime|log.Lshortfile)
86 | }
87 |
--------------------------------------------------------------------------------
/test/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | smb:
5 | image: dperson/samba
6 | environment:
7 | - USER=fregie;password
8 | - SHARE=photos;/tmp/photos;yes;no;no;fregie;fregie;fregie
9 | privileged: true
10 | ports:
11 | - "139:139"
12 | - "445:445"
13 | networks:
14 | - backend
15 |
16 | webdav:
17 | image: bytemark/webdav
18 | ports:
19 | - "8080:80"
20 | environment:
21 | AUTH_TYPE: Digest
22 | USERNAME: fregie
23 | PASSWORD: password
24 | networks:
25 | - backend
26 |
27 | nfs:
28 | image: erichough/nfs-server
29 | privileged: true
30 | cap_add:
31 | - SYS_ADMIN
32 | - SETPCAP
33 | volumes:
34 | - ./nfs/exports:/etc/exports:ro
35 | - nfs:/nfs
36 | networks:
37 | backend:
38 | ipv4_address: 192.168.23.10
39 |
40 | server:
41 | build: ../server
42 | ports:
43 | - "50051:50051"
44 | - "8000:8000"
45 | networks:
46 | - backend
47 |
48 | volumes:
49 | nfs:
50 |
51 | networks:
52 | backend:
53 | driver: bridge
54 | ipam:
55 | config:
56 | - subnet: 192.168.23.0/24
--------------------------------------------------------------------------------
/test/nfs/exports:
--------------------------------------------------------------------------------
1 | /nfs *(rw,sync,no_subtree_check,no_root_squash,insecure)
--------------------------------------------------------------------------------
/test/static/static.go:
--------------------------------------------------------------------------------
1 | package static
2 |
3 | import (
4 | _ "embed"
5 | "time"
6 | )
7 |
8 | //go:embed test_pic_01.jpg
9 | var Pic1 []byte
10 | var Pic1Name = "test_pic_01.png"
11 | var Pic1Data = time.Date(2022, 11, 8, 23, 56, 44, 0, time.FixedZone("UTC+8", 8*60*60))
12 |
--------------------------------------------------------------------------------
/test/static/test_pic_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/test/static/test_pic_01.jpg
--------------------------------------------------------------------------------
/web/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/web/favicon.png
--------------------------------------------------------------------------------
/web/icons/Icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/web/icons/Icon-192.png
--------------------------------------------------------------------------------
/web/icons/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/web/icons/Icon-512.png
--------------------------------------------------------------------------------
/web/icons/Icon-maskable-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/web/icons/Icon-maskable-192.png
--------------------------------------------------------------------------------
/web/icons/Icon-maskable-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/web/icons/Icon-maskable-512.png
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | img_syncer
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "img_syncer",
3 | "short_name": "img_syncer",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "A new Flutter project.",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [
12 | {
13 | "src": "icons/Icon-192.png",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 | {
18 | "src": "icons/Icon-512.png",
19 | "sizes": "512x512",
20 | "type": "image/png"
21 | },
22 | {
23 | "src": "icons/Icon-maskable-192.png",
24 | "sizes": "192x192",
25 | "type": "image/png",
26 | "purpose": "maskable"
27 | },
28 | {
29 | "src": "icons/Icon-maskable-512.png",
30 | "sizes": "512x512",
31 | "type": "image/png",
32 | "purpose": "maskable"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/windows/.gitignore:
--------------------------------------------------------------------------------
1 | flutter/ephemeral/
2 |
3 | # Visual Studio user-specific files.
4 | *.suo
5 | *.user
6 | *.userosscache
7 | *.sln.docstates
8 |
9 | # Visual Studio build-related files.
10 | x64/
11 | x86/
12 |
13 | # Visual Studio cache files
14 | # files ending in .cache can be ignored
15 | *.[Cc]ache
16 | # but keep track of directories ending in .cache
17 | !*.[Cc]ache/
18 |
--------------------------------------------------------------------------------
/windows/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # Project-level configuration.
2 | cmake_minimum_required(VERSION 3.14)
3 | project(img_syncer LANGUAGES CXX)
4 |
5 | # The name of the executable created for the application. Change this to change
6 | # the on-disk name of your application.
7 | set(BINARY_NAME "img_syncer")
8 |
9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
10 | # versions of CMake.
11 | cmake_policy(SET CMP0063 NEW)
12 |
13 | # Define build configuration option.
14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
15 | if(IS_MULTICONFIG)
16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
17 | CACHE STRING "" FORCE)
18 | else()
19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
20 | set(CMAKE_BUILD_TYPE "Debug" CACHE
21 | STRING "Flutter build mode" FORCE)
22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
23 | "Debug" "Profile" "Release")
24 | endif()
25 | endif()
26 | # Define settings for the Profile build mode.
27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
31 |
32 | # Use Unicode for all projects.
33 | add_definitions(-DUNICODE -D_UNICODE)
34 |
35 | # Compilation settings that should be applied to most targets.
36 | #
37 | # Be cautious about adding new options here, as plugins use this function by
38 | # default. In most cases, you should add new options to specific targets instead
39 | # of modifying this function.
40 | function(APPLY_STANDARD_SETTINGS TARGET)
41 | target_compile_features(${TARGET} PUBLIC cxx_std_17)
42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
43 | target_compile_options(${TARGET} PRIVATE /EHsc)
44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
46 | endfunction()
47 |
48 | # Flutter library and tool build rules.
49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
50 | add_subdirectory(${FLUTTER_MANAGED_DIR})
51 |
52 | # Application build; see runner/CMakeLists.txt.
53 | add_subdirectory("runner")
54 |
55 | # Generated plugin build rules, which manage building the plugins and adding
56 | # them to the application.
57 | include(flutter/generated_plugins.cmake)
58 |
59 |
60 | # === Installation ===
61 | # Support files are copied into place next to the executable, so that it can
62 | # run in place. This is done instead of making a separate bundle (as on Linux)
63 | # so that building and running from within Visual Studio will work.
64 | set(BUILD_BUNDLE_DIR "$")
65 | # Make the "install" step default, as it's required to run.
66 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
67 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
68 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
69 | endif()
70 |
71 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
72 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
73 |
74 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
75 | COMPONENT Runtime)
76 |
77 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
78 | COMPONENT Runtime)
79 |
80 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
81 | COMPONENT Runtime)
82 |
83 | if(PLUGIN_BUNDLED_LIBRARIES)
84 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
85 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
86 | COMPONENT Runtime)
87 | endif()
88 |
89 | # Fully re-copy the assets directory on each build to avoid having stale files
90 | # from a previous install.
91 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
92 | install(CODE "
93 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
94 | " COMPONENT Runtime)
95 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
96 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
97 |
98 | # Install the AOT library on non-Debug builds only.
99 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
100 | CONFIGURATIONS Profile;Release
101 | COMPONENT Runtime)
102 |
--------------------------------------------------------------------------------
/windows/flutter/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # This file controls Flutter-level build steps. It should not be edited.
2 | cmake_minimum_required(VERSION 3.14)
3 |
4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
5 |
6 | # Configuration provided via flutter tool.
7 | include(${EPHEMERAL_DIR}/generated_config.cmake)
8 |
9 | # TODO: Move the rest of this into files in ephemeral. See
10 | # https://github.com/flutter/flutter/issues/57146.
11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
12 |
13 | # === Flutter Library ===
14 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
15 |
16 | # Published to parent scope for install step.
17 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
18 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
19 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
20 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
21 |
22 | list(APPEND FLUTTER_LIBRARY_HEADERS
23 | "flutter_export.h"
24 | "flutter_windows.h"
25 | "flutter_messenger.h"
26 | "flutter_plugin_registrar.h"
27 | "flutter_texture_registrar.h"
28 | )
29 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
30 | add_library(flutter INTERFACE)
31 | target_include_directories(flutter INTERFACE
32 | "${EPHEMERAL_DIR}"
33 | )
34 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
35 | add_dependencies(flutter flutter_assemble)
36 |
37 | # === Wrapper ===
38 | list(APPEND CPP_WRAPPER_SOURCES_CORE
39 | "core_implementations.cc"
40 | "standard_codec.cc"
41 | )
42 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
43 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
44 | "plugin_registrar.cc"
45 | )
46 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
47 | list(APPEND CPP_WRAPPER_SOURCES_APP
48 | "flutter_engine.cc"
49 | "flutter_view_controller.cc"
50 | )
51 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
52 |
53 | # Wrapper sources needed for a plugin.
54 | add_library(flutter_wrapper_plugin STATIC
55 | ${CPP_WRAPPER_SOURCES_CORE}
56 | ${CPP_WRAPPER_SOURCES_PLUGIN}
57 | )
58 | apply_standard_settings(flutter_wrapper_plugin)
59 | set_target_properties(flutter_wrapper_plugin PROPERTIES
60 | POSITION_INDEPENDENT_CODE ON)
61 | set_target_properties(flutter_wrapper_plugin PROPERTIES
62 | CXX_VISIBILITY_PRESET hidden)
63 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
64 | target_include_directories(flutter_wrapper_plugin PUBLIC
65 | "${WRAPPER_ROOT}/include"
66 | )
67 | add_dependencies(flutter_wrapper_plugin flutter_assemble)
68 |
69 | # Wrapper sources needed for the runner.
70 | add_library(flutter_wrapper_app STATIC
71 | ${CPP_WRAPPER_SOURCES_CORE}
72 | ${CPP_WRAPPER_SOURCES_APP}
73 | )
74 | apply_standard_settings(flutter_wrapper_app)
75 | target_link_libraries(flutter_wrapper_app PUBLIC flutter)
76 | target_include_directories(flutter_wrapper_app PUBLIC
77 | "${WRAPPER_ROOT}/include"
78 | )
79 | add_dependencies(flutter_wrapper_app flutter_assemble)
80 |
81 | # === Flutter tool backend ===
82 | # _phony_ is a non-existent file to force this command to run every time,
83 | # since currently there's no way to get a full input/output list from the
84 | # flutter tool.
85 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
86 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
87 | add_custom_command(
88 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
89 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
90 | ${CPP_WRAPPER_SOURCES_APP}
91 | ${PHONY_OUTPUT}
92 | COMMAND ${CMAKE_COMMAND} -E env
93 | ${FLUTTER_TOOL_ENVIRONMENT}
94 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
95 | windows-x64 $
96 | VERBATIM
97 | )
98 | add_custom_target(flutter_assemble DEPENDS
99 | "${FLUTTER_LIBRARY}"
100 | ${FLUTTER_LIBRARY_HEADERS}
101 | ${CPP_WRAPPER_SOURCES_CORE}
102 | ${CPP_WRAPPER_SOURCES_PLUGIN}
103 | ${CPP_WRAPPER_SOURCES_APP}
104 | )
105 |
--------------------------------------------------------------------------------
/windows/flutter/generated_plugin_registrant.cc:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // clang-format off
6 |
7 | #include "generated_plugin_registrant.h"
8 |
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 |
15 | void RegisterPlugins(flutter::PluginRegistry* registry) {
16 | ConnectivityPlusWindowsPluginRegisterWithRegistrar(
17 | registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
18 | DynamicColorPluginCApiRegisterWithRegistrar(
19 | registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
20 | FileSelectorWindowsRegisterWithRegistrar(
21 | registry->GetRegistrarForPlugin("FileSelectorWindows"));
22 | SharePlusWindowsPluginCApiRegisterWithRegistrar(
23 | registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
24 | UrlLauncherWindowsRegisterWithRegistrar(
25 | registry->GetRegistrarForPlugin("UrlLauncherWindows"));
26 | }
27 |
--------------------------------------------------------------------------------
/windows/flutter/generated_plugin_registrant.h:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // clang-format off
6 |
7 | #ifndef GENERATED_PLUGIN_REGISTRANT_
8 | #define GENERATED_PLUGIN_REGISTRANT_
9 |
10 | #include
11 |
12 | // Registers Flutter plugins.
13 | void RegisterPlugins(flutter::PluginRegistry* registry);
14 |
15 | #endif // GENERATED_PLUGIN_REGISTRANT_
16 |
--------------------------------------------------------------------------------
/windows/flutter/generated_plugins.cmake:
--------------------------------------------------------------------------------
1 | #
2 | # Generated file, do not edit.
3 | #
4 |
5 | list(APPEND FLUTTER_PLUGIN_LIST
6 | connectivity_plus
7 | dynamic_color
8 | file_selector_windows
9 | share_plus
10 | url_launcher_windows
11 | )
12 |
13 | list(APPEND FLUTTER_FFI_PLUGIN_LIST
14 | )
15 |
16 | set(PLUGIN_BUNDLED_LIBRARIES)
17 |
18 | foreach(plugin ${FLUTTER_PLUGIN_LIST})
19 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
20 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
23 | endforeach(plugin)
24 |
25 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
26 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
27 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
28 | endforeach(ffi_plugin)
29 |
--------------------------------------------------------------------------------
/windows/runner/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.14)
2 | project(runner LANGUAGES CXX)
3 |
4 | # Define the application target. To change its name, change BINARY_NAME in the
5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
6 | # work.
7 | #
8 | # Any new source files that you add to the application should be added here.
9 | add_executable(${BINARY_NAME} WIN32
10 | "flutter_window.cpp"
11 | "main.cpp"
12 | "utils.cpp"
13 | "win32_window.cpp"
14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
15 | "Runner.rc"
16 | "runner.exe.manifest"
17 | )
18 |
19 | # Apply the standard set of build settings. This can be removed for applications
20 | # that need different build settings.
21 | apply_standard_settings(${BINARY_NAME})
22 |
23 | # Add preprocessor definitions for the build version.
24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
29 |
30 | # Disable Windows macros that collide with C++ standard library functions.
31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
32 |
33 | # Add dependency libraries and include directories. Add any application-specific
34 | # dependencies here.
35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
38 |
39 | # Run the Flutter tool portions of the build. This must not be removed.
40 | add_dependencies(${BINARY_NAME} flutter_assemble)
41 |
--------------------------------------------------------------------------------
/windows/runner/Runner.rc:
--------------------------------------------------------------------------------
1 | // Microsoft Visual C++ generated resource script.
2 | //
3 | #pragma code_page(65001)
4 | #include "resource.h"
5 |
6 | #define APSTUDIO_READONLY_SYMBOLS
7 | /////////////////////////////////////////////////////////////////////////////
8 | //
9 | // Generated from the TEXTINCLUDE 2 resource.
10 | //
11 | #include "winres.h"
12 |
13 | /////////////////////////////////////////////////////////////////////////////
14 | #undef APSTUDIO_READONLY_SYMBOLS
15 |
16 | /////////////////////////////////////////////////////////////////////////////
17 | // English (United States) resources
18 |
19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
21 |
22 | #ifdef APSTUDIO_INVOKED
23 | /////////////////////////////////////////////////////////////////////////////
24 | //
25 | // TEXTINCLUDE
26 | //
27 |
28 | 1 TEXTINCLUDE
29 | BEGIN
30 | "resource.h\0"
31 | END
32 |
33 | 2 TEXTINCLUDE
34 | BEGIN
35 | "#include ""winres.h""\r\n"
36 | "\0"
37 | END
38 |
39 | 3 TEXTINCLUDE
40 | BEGIN
41 | "\r\n"
42 | "\0"
43 | END
44 |
45 | #endif // APSTUDIO_INVOKED
46 |
47 |
48 | /////////////////////////////////////////////////////////////////////////////
49 | //
50 | // Icon
51 | //
52 |
53 | // Icon with lowest ID value placed first to ensure application icon
54 | // remains consistent on all systems.
55 | IDI_APP_ICON ICON "resources\\app_icon.ico"
56 |
57 |
58 | /////////////////////////////////////////////////////////////////////////////
59 | //
60 | // Version
61 | //
62 |
63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
65 | #else
66 | #define VERSION_AS_NUMBER 1,0,0,0
67 | #endif
68 |
69 | #if defined(FLUTTER_VERSION)
70 | #define VERSION_AS_STRING FLUTTER_VERSION
71 | #else
72 | #define VERSION_AS_STRING "1.0.0"
73 | #endif
74 |
75 | VS_VERSION_INFO VERSIONINFO
76 | FILEVERSION VERSION_AS_NUMBER
77 | PRODUCTVERSION VERSION_AS_NUMBER
78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
79 | #ifdef _DEBUG
80 | FILEFLAGS VS_FF_DEBUG
81 | #else
82 | FILEFLAGS 0x0L
83 | #endif
84 | FILEOS VOS__WINDOWS32
85 | FILETYPE VFT_APP
86 | FILESUBTYPE 0x0L
87 | BEGIN
88 | BLOCK "StringFileInfo"
89 | BEGIN
90 | BLOCK "040904e4"
91 | BEGIN
92 | VALUE "CompanyName", "com.example" "\0"
93 | VALUE "FileDescription", "img_syncer" "\0"
94 | VALUE "FileVersion", VERSION_AS_STRING "\0"
95 | VALUE "InternalName", "img_syncer" "\0"
96 | VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0"
97 | VALUE "OriginalFilename", "img_syncer.exe" "\0"
98 | VALUE "ProductName", "img_syncer" "\0"
99 | VALUE "ProductVersion", VERSION_AS_STRING "\0"
100 | END
101 | END
102 | BLOCK "VarFileInfo"
103 | BEGIN
104 | VALUE "Translation", 0x409, 1252
105 | END
106 | END
107 |
108 | #endif // English (United States) resources
109 | /////////////////////////////////////////////////////////////////////////////
110 |
111 |
112 |
113 | #ifndef APSTUDIO_INVOKED
114 | /////////////////////////////////////////////////////////////////////////////
115 | //
116 | // Generated from the TEXTINCLUDE 3 resource.
117 | //
118 |
119 |
120 | /////////////////////////////////////////////////////////////////////////////
121 | #endif // not APSTUDIO_INVOKED
122 |
--------------------------------------------------------------------------------
/windows/runner/flutter_window.cpp:
--------------------------------------------------------------------------------
1 | #include "flutter_window.h"
2 |
3 | #include
4 |
5 | #include "flutter/generated_plugin_registrant.h"
6 |
7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project)
8 | : project_(project) {}
9 |
10 | FlutterWindow::~FlutterWindow() {}
11 |
12 | bool FlutterWindow::OnCreate() {
13 | if (!Win32Window::OnCreate()) {
14 | return false;
15 | }
16 |
17 | RECT frame = GetClientArea();
18 |
19 | // The size here must match the window dimensions to avoid unnecessary surface
20 | // creation / destruction in the startup path.
21 | flutter_controller_ = std::make_unique(
22 | frame.right - frame.left, frame.bottom - frame.top, project_);
23 | // Ensure that basic setup of the controller was successful.
24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) {
25 | return false;
26 | }
27 | RegisterPlugins(flutter_controller_->engine());
28 | SetChildContent(flutter_controller_->view()->GetNativeWindow());
29 |
30 | flutter_controller_->engine()->SetNextFrameCallback([&]() {
31 | this->Show();
32 | });
33 |
34 | return true;
35 | }
36 |
37 | void FlutterWindow::OnDestroy() {
38 | if (flutter_controller_) {
39 | flutter_controller_ = nullptr;
40 | }
41 |
42 | Win32Window::OnDestroy();
43 | }
44 |
45 | LRESULT
46 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
47 | WPARAM const wparam,
48 | LPARAM const lparam) noexcept {
49 | // Give Flutter, including plugins, an opportunity to handle window messages.
50 | if (flutter_controller_) {
51 | std::optional result =
52 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
53 | lparam);
54 | if (result) {
55 | return *result;
56 | }
57 | }
58 |
59 | switch (message) {
60 | case WM_FONTCHANGE:
61 | flutter_controller_->engine()->ReloadSystemFonts();
62 | break;
63 | }
64 |
65 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
66 | }
67 |
--------------------------------------------------------------------------------
/windows/runner/flutter_window.h:
--------------------------------------------------------------------------------
1 | #ifndef RUNNER_FLUTTER_WINDOW_H_
2 | #define RUNNER_FLUTTER_WINDOW_H_
3 |
4 | #include
5 | #include
6 |
7 | #include
8 |
9 | #include "win32_window.h"
10 |
11 | // A window that does nothing but host a Flutter view.
12 | class FlutterWindow : public Win32Window {
13 | public:
14 | // Creates a new FlutterWindow hosting a Flutter view running |project|.
15 | explicit FlutterWindow(const flutter::DartProject& project);
16 | virtual ~FlutterWindow();
17 |
18 | protected:
19 | // Win32Window:
20 | bool OnCreate() override;
21 | void OnDestroy() override;
22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
23 | LPARAM const lparam) noexcept override;
24 |
25 | private:
26 | // The project to run.
27 | flutter::DartProject project_;
28 |
29 | // The Flutter instance hosted by this window.
30 | std::unique_ptr flutter_controller_;
31 | };
32 |
33 | #endif // RUNNER_FLUTTER_WINDOW_H_
34 |
--------------------------------------------------------------------------------
/windows/runner/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #include "flutter_window.h"
6 | #include "utils.h"
7 |
8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
9 | _In_ wchar_t *command_line, _In_ int show_command) {
10 | // Attach to console when present (e.g., 'flutter run') or create a
11 | // new console when running with a debugger.
12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
13 | CreateAndAttachConsole();
14 | }
15 |
16 | // Initialize COM, so that it is available for use in the library and/or
17 | // plugins.
18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
19 |
20 | flutter::DartProject project(L"data");
21 |
22 | std::vector command_line_arguments =
23 | GetCommandLineArguments();
24 |
25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
26 |
27 | FlutterWindow window(project);
28 | Win32Window::Point origin(10, 10);
29 | Win32Window::Size size(1280, 720);
30 | if (!window.Create(L"img_syncer", origin, size)) {
31 | return EXIT_FAILURE;
32 | }
33 | window.SetQuitOnClose(true);
34 |
35 | ::MSG msg;
36 | while (::GetMessage(&msg, nullptr, 0, 0)) {
37 | ::TranslateMessage(&msg);
38 | ::DispatchMessage(&msg);
39 | }
40 |
41 | ::CoUninitialize();
42 | return EXIT_SUCCESS;
43 | }
44 |
--------------------------------------------------------------------------------
/windows/runner/resource.h:
--------------------------------------------------------------------------------
1 | //{{NO_DEPENDENCIES}}
2 | // Microsoft Visual C++ generated include file.
3 | // Used by Runner.rc
4 | //
5 | #define IDI_APP_ICON 101
6 |
7 | // Next default values for new objects
8 | //
9 | #ifdef APSTUDIO_INVOKED
10 | #ifndef APSTUDIO_READONLY_SYMBOLS
11 | #define _APS_NEXT_RESOURCE_VALUE 102
12 | #define _APS_NEXT_COMMAND_VALUE 40001
13 | #define _APS_NEXT_CONTROL_VALUE 1001
14 | #define _APS_NEXT_SYMED_VALUE 101
15 | #endif
16 | #endif
17 |
--------------------------------------------------------------------------------
/windows/runner/resources/app_icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fregie/pho/52c8dbefa4e109339bfb8875561f186646edb337/windows/runner/resources/app_icon.ico
--------------------------------------------------------------------------------
/windows/runner/runner.exe.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PerMonitorV2
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/windows/runner/utils.cpp:
--------------------------------------------------------------------------------
1 | #include "utils.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | #include
9 |
10 | void CreateAndAttachConsole() {
11 | if (::AllocConsole()) {
12 | FILE *unused;
13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
14 | _dup2(_fileno(stdout), 1);
15 | }
16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
17 | _dup2(_fileno(stdout), 2);
18 | }
19 | std::ios::sync_with_stdio();
20 | FlutterDesktopResyncOutputStreams();
21 | }
22 | }
23 |
24 | std::vector GetCommandLineArguments() {
25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
26 | int argc;
27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
28 | if (argv == nullptr) {
29 | return std::vector();
30 | }
31 |
32 | std::vector command_line_arguments;
33 |
34 | // Skip the first argument as it's the binary name.
35 | for (int i = 1; i < argc; i++) {
36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
37 | }
38 |
39 | ::LocalFree(argv);
40 |
41 | return command_line_arguments;
42 | }
43 |
44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) {
45 | if (utf16_string == nullptr) {
46 | return std::string();
47 | }
48 | int target_length = ::WideCharToMultiByte(
49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
50 | -1, nullptr, 0, nullptr, nullptr);
51 | std::string utf8_string;
52 | if (target_length == 0 || target_length > utf8_string.max_size()) {
53 | return utf8_string;
54 | }
55 | utf8_string.resize(target_length);
56 | int converted_length = ::WideCharToMultiByte(
57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
58 | -1, utf8_string.data(),
59 | target_length, nullptr, nullptr);
60 | if (converted_length == 0) {
61 | return std::string();
62 | }
63 | return utf8_string;
64 | }
65 |
--------------------------------------------------------------------------------
/windows/runner/utils.h:
--------------------------------------------------------------------------------
1 | #ifndef RUNNER_UTILS_H_
2 | #define RUNNER_UTILS_H_
3 |
4 | #include
5 | #include
6 |
7 | // Creates a console for the process, and redirects stdout and stderr to
8 | // it for both the runner and the Flutter library.
9 | void CreateAndAttachConsole();
10 |
11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
12 | // encoded in UTF-8. Returns an empty std::string on failure.
13 | std::string Utf8FromUtf16(const wchar_t* utf16_string);
14 |
15 | // Gets the command line arguments passed in as a std::vector,
16 | // encoded in UTF-8. Returns an empty std::vector on failure.
17 | std::vector GetCommandLineArguments();
18 |
19 | #endif // RUNNER_UTILS_H_
20 |
--------------------------------------------------------------------------------
/windows/runner/win32_window.h:
--------------------------------------------------------------------------------
1 | #ifndef RUNNER_WIN32_WINDOW_H_
2 | #define RUNNER_WIN32_WINDOW_H_
3 |
4 | #include
5 |
6 | #include
7 | #include
8 | #include
9 |
10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be
11 | // inherited from by classes that wish to specialize with custom
12 | // rendering and input handling
13 | class Win32Window {
14 | public:
15 | struct Point {
16 | unsigned int x;
17 | unsigned int y;
18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {}
19 | };
20 |
21 | struct Size {
22 | unsigned int width;
23 | unsigned int height;
24 | Size(unsigned int width, unsigned int height)
25 | : width(width), height(height) {}
26 | };
27 |
28 | Win32Window();
29 | virtual ~Win32Window();
30 |
31 | // Creates a win32 window with |title| that is positioned and sized using
32 | // |origin| and |size|. New windows are created on the default monitor. Window
33 | // sizes are specified to the OS in physical pixels, hence to ensure a
34 | // consistent size this function will scale the inputted width and height as
35 | // as appropriate for the default monitor. The window is invisible until
36 | // |Show| is called. Returns true if the window was created successfully.
37 | bool Create(const std::wstring& title, const Point& origin, const Size& size);
38 |
39 | // Show the current window. Returns true if the window was successfully shown.
40 | bool Show();
41 |
42 | // Release OS resources associated with window.
43 | void Destroy();
44 |
45 | // Inserts |content| into the window tree.
46 | void SetChildContent(HWND content);
47 |
48 | // Returns the backing Window handle to enable clients to set icon and other
49 | // window properties. Returns nullptr if the window has been destroyed.
50 | HWND GetHandle();
51 |
52 | // If true, closing this window will quit the application.
53 | void SetQuitOnClose(bool quit_on_close);
54 |
55 | // Return a RECT representing the bounds of the current client area.
56 | RECT GetClientArea();
57 |
58 | protected:
59 | // Processes and route salient window messages for mouse handling,
60 | // size change and DPI. Delegates handling of these to member overloads that
61 | // inheriting classes can handle.
62 | virtual LRESULT MessageHandler(HWND window,
63 | UINT const message,
64 | WPARAM const wparam,
65 | LPARAM const lparam) noexcept;
66 |
67 | // Called when CreateAndShow is called, allowing subclass window-related
68 | // setup. Subclasses should return false if setup fails.
69 | virtual bool OnCreate();
70 |
71 | // Called when Destroy is called.
72 | virtual void OnDestroy();
73 |
74 | private:
75 | friend class WindowClassRegistrar;
76 |
77 | // OS callback called by message pump. Handles the WM_NCCREATE message which
78 | // is passed when the non-client area is being created and enables automatic
79 | // non-client DPI scaling so that the non-client area automatically
80 | // responsponds to changes in DPI. All other messages are handled by
81 | // MessageHandler.
82 | static LRESULT CALLBACK WndProc(HWND const window,
83 | UINT const message,
84 | WPARAM const wparam,
85 | LPARAM const lparam) noexcept;
86 |
87 | // Retrieves a class instance pointer for |window|
88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept;
89 |
90 | // Update the window frame's theme to match the system theme.
91 | static void UpdateTheme(HWND const window);
92 |
93 | bool quit_on_close_ = false;
94 |
95 | // window handle for top level window.
96 | HWND window_handle_ = nullptr;
97 |
98 | // window handle for hosted content.
99 | HWND child_content_ = nullptr;
100 | };
101 |
102 | #endif // RUNNER_WIN32_WINDOW_H_
103 |
--------------------------------------------------------------------------------