├── files-web
├── src
│ ├── @types
│ │ └── declaration.d.ts
│ ├── react-app-env.d.ts
│ ├── types
│ │ └── File.ts
│ ├── containers
│ │ ├── Home
│ │ │ └── index.tsx
│ │ ├── Photo
│ │ │ └── index.tsx
│ │ ├── Video
│ │ │ └── index.tsx
│ │ └── DocumentBrowser
│ │ │ ├── styled.tsx
│ │ │ └── index.tsx
│ ├── App.tsx
│ ├── App.test.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── App.css
│ ├── components
│ │ └── Router.tsx
│ ├── services
│ │ └── request.ts
│ ├── utils
│ │ └── util.ts
│ └── serviceWorker.ts
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── .gitignore
├── tsconfig.json
├── package.json
└── README.md
├── .gitmodules
├── Files
├── Assets.xcassets
│ ├── Contents.json
│ ├── Music
│ │ ├── Contents.json
│ │ └── icon_arrow_down.imageset
│ │ │ ├── icon-arrow-o-down.png
│ │ │ ├── icon-arrow-o-down-1.png
│ │ │ └── Contents.json
│ ├── FileIcon
│ │ ├── Contents.json
│ │ ├── icon_zip.imageset
│ │ │ ├── zip.png
│ │ │ ├── zip-1.png
│ │ │ └── Contents.json
│ │ ├── icon_audio.imageset
│ │ │ ├── 2010061608274810.png
│ │ │ ├── 2010061608274810-1.png
│ │ │ └── Contents.json
│ │ ├── icon_video.imageset
│ │ │ ├── 2010061608274899.png
│ │ │ ├── 2010061608274899 (1).png
│ │ │ └── Contents.json
│ │ ├── icon_unknown.imageset
│ │ │ ├── daaac63be1b6388034d3f70d90a9497b.png
│ │ │ ├── daaac63be1b6388034d3f70d90a9497b-1.png
│ │ │ └── Contents.json
│ │ └── icon_directory.imageset
│ │ │ ├── 48d9e9bdcf89243e6520fba2f96b9bd7.png
│ │ │ └── Contents.json
│ ├── SideMenu
│ │ ├── Contents.json
│ │ └── SideMenuBackground.imageset
│ │ │ ├── 59163449_p0.png
│ │ │ └── Contents.json
│ ├── MediaPlayer
│ │ ├── Contents.json
│ │ ├── icon_media_player_back.imageset
│ │ │ ├── hd_idct_back.png
│ │ │ └── Contents.json
│ │ ├── icon_media_player_play.imageset
│ │ │ ├── player_play@2x.png
│ │ │ ├── player_play@3x.png
│ │ │ └── Contents.json
│ │ └── icon_media_player_pause.imageset
│ │ │ ├── player_pause@2x.png
│ │ │ ├── player_pause@3x.png
│ │ │ └── Contents.json
│ ├── MusicPlayer
│ │ ├── Contents.json
│ │ ├── icon-next.imageset
│ │ │ ├── next_song.png
│ │ │ └── Contents.json
│ │ ├── icon-prev.imageset
│ │ │ ├── prev_song.png
│ │ │ └── Contents.json
│ │ ├── icon_more.imageset
│ │ │ ├── more_icon.png
│ │ │ └── Contents.json
│ │ ├── icon-play.imageset
│ │ │ ├── big_play_button.png
│ │ │ └── Contents.json
│ │ ├── icon_random.imageset
│ │ │ ├── shuffle_icon.png
│ │ │ └── Contents.json
│ │ ├── icon-pause.imageset
│ │ │ ├── big_pause_button.png
│ │ │ └── Contents.json
│ │ ├── icon_loopAll.imageset
│ │ │ ├── loop_all_icon.png
│ │ │ └── Contents.json
│ │ └── icon_loopSingle.imageset
│ │ │ ├── loop_single_icon.png
│ │ │ └── Contents.json
│ ├── Stars.imageset
│ │ ├── Stars@2x.png
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── IMG_0316.png
│ │ └── Contents.json
│ ├── icon_choose_n.imageset
│ │ ├── list_a_choose@2x.png
│ │ ├── list_a_choose@3x.png
│ │ └── Contents.json
│ └── icon_choose_y.imageset
│ │ ├── list_a_choose_s@2x.png
│ │ ├── list_a_choose_s@3x.png
│ │ └── Contents.json
├── Files-Bridging-Header.h
├── Extensions
│ ├── ArrayExtension.swift
│ ├── StringExtension.swift
│ ├── TimeIntervalExtension.swift
│ ├── UIViewExtensions.swift
│ ├── UIImageExtension.swift
│ └── UIResponderExtensions.swift
├── ViewController.swift
├── DataTransfer
│ ├── HTTPDataResponse.swift
│ ├── HTTPAsyncFileResponse.swift
│ ├── FileHTTPServer.swift
│ └── FileHTTPConnection.swift
├── File
│ ├── View
│ │ ├── DocumentBrowserFlowLayout.swift
│ │ ├── DocumentBrowserControlView.swift
│ │ ├── FileListFlowLayout.swift
│ │ ├── DocumentBrowserToolBar.swift
│ │ └── DocumentCollectionViewCell.swift
│ ├── File.swift
│ ├── FileThumbnailCache.swift
│ ├── Document.swift
│ └── DocumentDirectoryPickerViewController.swift
├── Music
│ ├── MusicPlayer
│ │ ├── AVAudioExtensions.swift
│ │ ├── MusicPlayer+Types.swift
│ │ ├── MusicPlayer+MPNowPlayingInfo.swift
│ │ ├── MusicPlayer+RemoteComtrol.swift
│ │ └── MusicPlayer.swift
│ ├── MusicFileType.swift
│ ├── View
│ │ ├── MusicIndicatorView.swift
│ │ └── MusicPlayerControlView.swift
│ ├── Music.swift
│ ├── Spectrum
│ │ ├── SpectrumView.swift
│ │ └── SpectrumAnalysis.swift
│ └── MusicPlayerViewController.swift
├── Video
│ ├── MediaPlayer
│ │ ├── MediaPlayerControlAble.swift
│ │ ├── MediaPlayerUtils.swift
│ │ ├── MediaPlayerTypes.swift
│ │ ├── MediaPlayerProgressView.swift
│ │ ├── MediaPlayerProgressHUD.swift
│ │ ├── MediaPlayerViewController.swift
│ │ ├── MediaPlayerGestureRecognizer.swift
│ │ └── MediaPlayerView.swift
│ └── VideoFileType.swift
├── Common
│ ├── Utils.swift
│ ├── Consts.swift
│ ├── StringExtensions.swift
│ └── TableUpdate.swift
├── Photo
│ ├── PhotoFileType.swift
│ ├── PhotoBrowserViewController.swift
│ └── PhotoPrefetchManager.swift
├── Zip
│ └── ZipFileType.swift
├── Base.lproj
│ ├── Main.storyboard
│ └── LaunchScreen.storyboard
├── Info.plist
└── AppDelegate.swift
├── Files.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ └── Files.xcscheme
├── Files.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Podfile
├── FilesTests
├── Info.plist
└── FilesTests.swift
├── LICENSE
├── README.md
├── .gitignore
└── Podfile.lock
/files-web/src/@types/declaration.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'video-react';
2 |
--------------------------------------------------------------------------------
/files-web/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/files-web/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "ijkplayer"]
2 | path = ijkplayer
3 | url = https://github.com/cezres/ijkplayer.git
4 |
--------------------------------------------------------------------------------
/files-web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/files-web/public/favicon.ico
--------------------------------------------------------------------------------
/Files/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/Music/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/SideMenu/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MediaPlayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/Stars.imageset/Stars@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/Stars.imageset/Stars@2x.png
--------------------------------------------------------------------------------
/Files/Files-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/AppIcon.appiconset/IMG_0316.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/AppIcon.appiconset/IMG_0316.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_zip.imageset/zip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/FileIcon/icon_zip.imageset/zip.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_zip.imageset/zip-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/FileIcon/icon_zip.imageset/zip-1.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon-next.imageset/next_song.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MusicPlayer/icon-next.imageset/next_song.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon-prev.imageset/prev_song.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MusicPlayer/icon-prev.imageset/prev_song.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon_more.imageset/more_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MusicPlayer/icon_more.imageset/more_icon.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/icon_choose_n.imageset/list_a_choose@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/icon_choose_n.imageset/list_a_choose@2x.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/icon_choose_n.imageset/list_a_choose@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/icon_choose_n.imageset/list_a_choose@3x.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/icon_choose_y.imageset/list_a_choose_s@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/icon_choose_y.imageset/list_a_choose_s@2x.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/icon_choose_y.imageset/list_a_choose_s@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/icon_choose_y.imageset/list_a_choose_s@3x.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_audio.imageset/2010061608274810.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/FileIcon/icon_audio.imageset/2010061608274810.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_video.imageset/2010061608274899.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/FileIcon/icon_video.imageset/2010061608274899.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon-play.imageset/big_play_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MusicPlayer/icon-play.imageset/big_play_button.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon_random.imageset/shuffle_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MusicPlayer/icon_random.imageset/shuffle_icon.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_audio.imageset/2010061608274810-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/FileIcon/icon_audio.imageset/2010061608274810-1.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/Music/icon_arrow_down.imageset/icon-arrow-o-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/Music/icon_arrow_down.imageset/icon-arrow-o-down.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon-pause.imageset/big_pause_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MusicPlayer/icon-pause.imageset/big_pause_button.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon_loopAll.imageset/loop_all_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MusicPlayer/icon_loopAll.imageset/loop_all_icon.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/SideMenu/SideMenuBackground.imageset/59163449_p0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/SideMenu/SideMenuBackground.imageset/59163449_p0.png
--------------------------------------------------------------------------------
/files-web/src/types/File.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface File {
3 | path: string
4 | icon: string
5 | type: string
6 | name: string
7 | size: number
8 | modificationDate: number
9 | }
10 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_video.imageset/2010061608274899 (1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/FileIcon/icon_video.imageset/2010061608274899 (1).png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/Music/icon_arrow_down.imageset/icon-arrow-o-down-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/Music/icon_arrow_down.imageset/icon-arrow-o-down-1.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon_loopSingle.imageset/loop_single_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MusicPlayer/icon_loopSingle.imageset/loop_single_icon.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MediaPlayer/icon_media_player_back.imageset/hd_idct_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MediaPlayer/icon_media_player_back.imageset/hd_idct_back.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MediaPlayer/icon_media_player_play.imageset/player_play@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MediaPlayer/icon_media_player_play.imageset/player_play@2x.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MediaPlayer/icon_media_player_play.imageset/player_play@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MediaPlayer/icon_media_player_play.imageset/player_play@3x.png
--------------------------------------------------------------------------------
/files-web/src/containers/Home/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 |
3 | export default () => {
4 | useEffect(() => {
5 | window.location.href = `/files`
6 | }, [])
7 | return
8 | }
9 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MediaPlayer/icon_media_player_pause.imageset/player_pause@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MediaPlayer/icon_media_player_pause.imageset/player_pause@2x.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MediaPlayer/icon_media_player_pause.imageset/player_pause@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/MediaPlayer/icon_media_player_pause.imageset/player_pause@3x.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_unknown.imageset/daaac63be1b6388034d3f70d90a9497b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/FileIcon/icon_unknown.imageset/daaac63be1b6388034d3f70d90a9497b.png
--------------------------------------------------------------------------------
/Files.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_directory.imageset/48d9e9bdcf89243e6520fba2f96b9bd7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/FileIcon/icon_directory.imageset/48d9e9bdcf89243e6520fba2f96b9bd7.png
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_unknown.imageset/daaac63be1b6388034d3f70d90a9497b-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cezres/Files-Swift/HEAD/Files/Assets.xcassets/FileIcon/icon_unknown.imageset/daaac63be1b6388034d3f70d90a9497b-1.png
--------------------------------------------------------------------------------
/files-web/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.css';
3 | import Router from './components/Router';
4 |
5 | const App: React.FC = () => {
6 | return (
7 |
8 | );
9 | }
10 |
11 | export default App;
12 |
--------------------------------------------------------------------------------
/Files.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/files-web/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/Files.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Files.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Files/Extensions/ArrayExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArrayExtension.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/19.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Array {
12 | var ns: NSArray {
13 | return NSArray(array: self)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Files/Extensions/StringExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringExtension.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | extension String {
13 | var fileURL: URL {
14 | return URL(fileURLWithPath: self)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/files-web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/Stars.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "Stars@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/files-web/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/Files/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ViewController: UIViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 | // Do any additional setup after loading the view, typically from a nib.
16 | }
17 |
18 |
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon-next.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "next_song.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon-prev.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "prev_song.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon_more.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "more_icon.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/DataTransfer/HTTPDataResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPResponse.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/9/16.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CocoaHTTPServer
11 |
12 | class HTTPDataResponse: CocoaHTTPServer.HTTPDataResponse {
13 | override func httpHeaders() -> [AnyHashable : Any]! {
14 | return ["Access-Control-Allow-Origin": "*"]
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon_random.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "shuffle_icon.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon-pause.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "big_pause_button.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon-play.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "big_play_button.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon_loopAll.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "loop_all_icon.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/SideMenu/SideMenuBackground.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "59163449_p0.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MusicPlayer/icon_loopSingle.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "loop_single_icon.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MediaPlayer/icon_media_player_back.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "universal",
13 | "filename" : "hd_idct_back.png",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_directory.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "48d9e9bdcf89243e6520fba2f96b9bd7.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Files/DataTransfer/HTTPAsyncFileResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPAsyncFileResponse.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/9/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CocoaHTTPServer
11 |
12 | class HTTPAsyncFileResponse: CocoaHTTPServer.HTTPAsyncFileResponse {
13 | override func httpHeaders() -> [AnyHashable : Any]! {
14 | return ["Access-Control-Allow-Origin": "*"]
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_zip.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "zip-1.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "zip.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/icon_choose_n.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "list_a_choose@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "list_a_choose@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_audio.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "2010061608274810-1.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "2010061608274810.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/icon_choose_y.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "list_a_choose_s@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "list_a_choose_s@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_video.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "2010061608274899.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "2010061608274899 (1).png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/Music/icon_arrow_down.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "icon-arrow-o-down-1.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "icon-arrow-o-down.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MediaPlayer/icon_media_player_play.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "player_play@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "player_play@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/files-web/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/MediaPlayer/icon_media_player_pause.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "player_pause@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "player_pause@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Files/Assets.xcassets/FileIcon/icon_unknown.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "daaac63be1b6388034d3f70d90a9497b-1.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "daaac63be1b6388034d3f70d90a9497b.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/files-web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/files-web/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | inhibit_all_warnings!
2 | platform :ios, '12.0'
3 | use_frameworks!
4 | #install! 'cocoapods', :generate_multiple_pod_projects => true
5 |
6 |
7 | target 'Files' do
8 | pod 'CocoaHTTPServer', git: "https://github.com/cezres/CocoaHTTPServer.git", commit: "d25f6f1384c4222763260d7f597d4e304c507631"
9 | pod 'SnapKit'
10 | pod 'FastImageCache'
11 | pod 'ESTMusicIndicator'
12 | pod 'Toast-Swift'
13 | pod 'Zip'
14 | pod 'WatchFolder', git: "https://github.com/cezres/WatchFolder.git", commit: "b869c3fc7daba4c5a85e7bd56fa7681b49c0ce54"
15 | pod 'DiffableDataSources'
16 | end
17 |
18 | target 'FilesTests' do
19 | end
20 |
21 |
--------------------------------------------------------------------------------
/files-web/src/containers/Photo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { getUrlParams } from '../../utils/util'
3 | import { baseURL } from '../../services/request'
4 | import styled from 'styled-components'
5 |
6 | const PhotoPanel = styled.div`
7 | width: 100vw;
8 | height: 100vh;
9 |
10 | >img {
11 | max-width: 100%;
12 | max-height: 100%;
13 | }
14 | `
15 |
16 | export default (props: any) => {
17 | const params = getUrlParams(props.location)
18 | let path = params.path
19 |
20 | return (
21 |
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/files-web/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | }
10 |
11 | .App-header {
12 | background-color: #282c34;
13 | min-height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | font-size: calc(10px + 2vmin);
19 | color: white;
20 | }
21 |
22 | .App-link {
23 | color: #61dafb;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Files/Extensions/TimeIntervalExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeInterval.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/28.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension TimeInterval {
12 | func formatterToTime() -> String {
13 | let time = lround(self)
14 | let hour = time / 3600
15 | let minute = (time - hour * 3600) / 60
16 | let second = time - hour * 3600 - minute * 60
17 | if hour > 0 {
18 | return String(format: "%02d:%02d:%02d", hour, minute, second)
19 | } else {
20 | return String(format: "%02d:%02d", minute, second)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Files/File/View/DocumentBrowserFlowLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DocumentBrowserFlowLayout.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/8.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol DocumentBrowserFlowLayout {
12 | var isEditing: Bool { get set }
13 | var delegate: DocumentBrowserFlowLayoutDelegate? { get set }
14 | func cellForItem(at indexPath: IndexPath) -> UICollectionViewCell
15 | }
16 |
17 | protocol DocumentBrowserFlowLayoutDelegate: class {
18 | func flowLayout(_ flowLayout: DocumentBrowserFlowLayout, fileForItemAt indexPath: IndexPath) -> File
19 | func flowLayout(_ flowLayout: DocumentBrowserFlowLayout, isSelectedAt indexPath: IndexPath) -> Bool
20 | }
21 |
--------------------------------------------------------------------------------
/Files/Music/MusicPlayer/AVAudioExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AVAudioExtensions.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/9/12.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 |
12 | extension AVAudioTime {
13 | var timeInterval: TimeInterval {
14 | return Double(sampleTime) / sampleRate
15 | }
16 | }
17 |
18 | extension AVAudioPlayerNode {
19 | var lastPlayTime: AVAudioTime? {
20 | guard let nodeTime = lastRenderTime else { return nil }
21 | return playerTime(forNodeTime: nodeTime)
22 | }
23 | }
24 |
25 | extension AVAudioFile {
26 | var duration: TimeInterval {
27 | return TimeInterval(length) / fileFormat.sampleRate
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Files/Video/MediaPlayer/MediaPlayerControlAble.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MediaPlayerControlAble.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/26.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol MediaPlayerCtrlAble {
12 | var playerView: MediaPlayerView! { get set }
13 |
14 | func reset()
15 |
16 | func cleanup()
17 |
18 | // MAKR: - View
19 | var isCanHideCtrlView: Bool { get }
20 |
21 | func setControlViewHidden(_ hidden: Bool)
22 |
23 | func layoutView(for bounds: CGRect)
24 |
25 | // MAKR: - State
26 | func playerDidChangeLoadState(_ loadState: MediaPlayerView.LoadState)
27 |
28 | func playerDidChangePlayBackState(_ backState: MediaPlayerView.PlaybackState)
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/Files/Video/MediaPlayer/MediaPlayerUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MediaPlayerUtils.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/26.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import MobileCoreServices
11 |
12 | extension MediaPlayerView {
13 | static func MIMEType(with file: URL) -> String? {
14 | guard FileManager.default.fileExists(atPath: file.path) else { return nil }
15 | guard let UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, file.pathExtension as CFString, nil) else { return nil }
16 | let MIMEType = UTTypeCopyPreferredTagWithClass(UTI.takeUnretainedValue() , kUTTagClassMIMEType)
17 | return MIMEType?.takeUnretainedValue() as String?
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/files-web/src/containers/Video/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useLayoutEffect } from 'react'
2 | import { Player } from 'video-react'
3 | import { getUrlParams } from '../../utils/util'
4 | import { baseURL } from '../../services/request'
5 | import 'video-react/dist/video-react.css'
6 | import styled from 'styled-components'
7 |
8 | const VideoPanel = styled.div`
9 | width: 100vw;
10 | height: 100vh;
11 | `
12 |
13 | export default (props: any) => {
14 | const params = getUrlParams(props.location)
15 | let path = params.path
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/FilesTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Files/Music/MusicPlayer/MusicPlayer+Types.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicPlayer+Types.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/24.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension MusicPlayer {
12 | enum State {
13 | case playing
14 | case stopped
15 | case paused
16 | }
17 |
18 | enum PlayMode {
19 | case loopAll
20 | case loopSingle
21 | case random
22 | }
23 |
24 | struct Notification {
25 | static let didChangeState = NSNotification.Name("MusicPlayer.didChangeState")
26 | static let didChangeMusic = NSNotification.Name("MusicPlayer.didChangeMusic")
27 | static let didReceivePCMBuffer = NSNotification.Name("MusicPlayer.didReceivePCMBuffer") // userInfo: ["buffer": buffer]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/files-web/src/components/Router.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter, Route, Switch } from 'react-router-dom';
3 | import Home from '../containers/Home';
4 | import DocumentBrowser from '../containers/DocumentBrowser'
5 | import Photo from '../containers/Photo'
6 | import Video from '../containers/Video'
7 |
8 | export enum Routes {
9 | DocumentBrowser = '/files',
10 | Photo = "/photo",
11 | Video = "/video",
12 | Home = '/',
13 | }
14 |
15 | export default () => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/Files/Common/Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utils.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/9/7.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | func makeToastToWindow(code: () throws -> Void) {
12 | do {
13 | try code()
14 | } catch {
15 | UIApplication.shared.keyWindow?.makeToast(error.localizedDescription)
16 | }
17 | }
18 |
19 | func generateFilePath(name: String, pathExtension: String, directory: URL) -> URL {
20 | var filePath = directory.appendingPathComponent(name + (pathExtension.count > 0 ? ".\(pathExtension)" : ""))
21 | var flag = 1
22 | while FileManager.default.fileExists(atPath: filePath.path) {
23 | if pathExtension == "" {
24 | filePath = directory.appendingPathComponent("\(name)\(flag)")
25 | } else {
26 | filePath = directory.appendingPathComponent("\(name)\(flag).\(pathExtension)")
27 | }
28 | flag += 1
29 | }
30 | return filePath
31 | }
32 |
--------------------------------------------------------------------------------
/FilesTests/FilesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilesTests.swift
3 | // FilesTests
4 | //
5 | // Created by 翟泉 on 2019/3/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Files
11 |
12 | class FilesTests: XCTestCase {
13 |
14 | override func setUp() {
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDown() {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | }
21 |
22 | func testExample() {
23 | // This is an example of a functional test case.
24 | // Use XCTAssert and related functions to verify your tests produce the correct results.
25 | }
26 |
27 | func testPerformanceExample() {
28 | // This is an example of a performance test case.
29 | self.measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Files/Video/VideoFileType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoFileType.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/2.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import IJKMediaFramework
11 |
12 | struct VideoFileType: FileType {
13 | let name = "Video"
14 | let pathExtensions = ["mp4", "flv"]
15 |
16 | func thumbnail(file: File, completion: @escaping (UIImage) -> Void) {
17 | FileThumbnailCache.shared.retrieveImage(identifier: file.identifier, sourceImage: { () -> UIImage? in
18 | let result = IJKFFMovieScreenshot.screenshot(withVideo: file.url.path, forSeconds: 4)
19 | return result ?? UIImage(named: "icon_video")
20 | }) { (_, image) in
21 | completion(image ?? UIImage(named: "icon_video")!)
22 | }
23 | }
24 |
25 | func openFile(_ file: File, document: Document, controller: DocumentBrowserViewController) {
26 | let mediaPlayer = MediaPlayerViewController(url: file.url)
27 | controller.present(mediaPlayer, animated: true, completion: nil)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 晨风
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Files/Music/MusicPlayer/MusicPlayer+MPNowPlayingInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicPlayer+MPNowPlayingInfo.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/24.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import MediaPlayer
11 |
12 | extension MusicPlayer {
13 | func configNowPlayingInfoCenter() {
14 | var info = [String: Any]()
15 | defer {
16 | MPNowPlayingInfoCenter.default().nowPlayingInfo = info
17 | }
18 | guard let metadata = music?.metadata else { return }
19 | info[MPMediaItemPropertyTitle] = metadata.song
20 | info[MPMediaItemPropertyArtist] = metadata.singer
21 | info[MPMediaItemPropertyAlbumTitle] = metadata.albumName
22 | info[MPMediaItemPropertyPlaybackDuration] = NSNumber(value: metadata.duration)
23 | info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: currentTime)
24 | if let artworkImage = metadata.artwork {
25 | info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: artworkImage.size, requestHandler: { _ in artworkImage })
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/files-web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "files-web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "./",
6 | "dependencies": {
7 | "@types/jest": "24.0.18",
8 | "@types/node": "12.7.5",
9 | "@types/react": "16.9.2",
10 | "@types/react-dom": "16.9.0",
11 | "axios": "^0.19.0",
12 | "camelcase-keys": "^6.0.1",
13 | "react": "^16.9.0",
14 | "react-dom": "^16.9.0",
15 | "react-router-dom": "^5.0.1",
16 | "react-scripts": "3.1.1",
17 | "rsuite": "^4.0.0",
18 | "styled-components": "^4.3.2",
19 | "typescript": "3.6.3",
20 | "video-react": "^0.14.1"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": "react-app"
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | },
43 | "devDependencies": {
44 | "@types/react-router-dom": "^4.3.5",
45 | "@types/styled-components": "^4.1.19"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Files/Photo/PhotoFileType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoFileType.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/19.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | struct PhotoFileType: FileType {
13 | let name = "Photo"
14 | let pathExtensions = ["jpg", "png"]
15 |
16 | func thumbnail(file: File, completion: @escaping (UIImage) -> Void) {
17 | FileThumbnailCache.shared.retrieveImage(identifier: file.identifier, sourceImage: { () -> UIImage? in
18 | return UIImage(contentsOfFile: file.url.path)
19 | }) { (_, image) in
20 | completion(image!)
21 | }
22 | }
23 |
24 | func openFile(_ file: File, document: Document, controller: DocumentBrowserViewController) {
25 | var files = [File]()
26 | var index: Int?
27 | document.contents.forEach({
28 | guard $0.type is PhotoFileType else { return }
29 | files.append($0)
30 | guard index == nil && file == $0 else { return }
31 | index = files.count - 1
32 | })
33 | let photoBrowser = PhotoBrowserViewController(files: files, index: index!)
34 | controller.navigationController?.pushViewController(photoBrowser, animated: true)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Files/Common/Consts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Consts.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | // MARK: - Path
13 |
14 | let HomeDirectory = NSHomeDirectory().fileURL
15 |
16 | let DocumentDirectory = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0].fileURL
17 |
18 | let CachesDirectory = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0].fileURL
19 |
20 | // MARK: - Color
21 | func ColorWhite(_ white: CGFloat) -> UIColor {
22 | return ColorWhiteAlpha(white, 1.0)
23 | }
24 |
25 | func ColorWhiteAlpha(_ white: CGFloat, _ alpha: CGFloat) -> UIColor {
26 | return UIColor(white: white/255.0, alpha: alpha)
27 | }
28 |
29 | func ColorRGB(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> UIColor {
30 | return ColorRGBA(r, g, b, 1.0)
31 | }
32 |
33 | func ColorRGBA(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat, _ a: CGFloat) -> UIColor {
34 | return UIColor(red: r/255.0, green: g/255.0, blue: b/255.0, alpha: a)
35 | }
36 |
37 | // MARK: - Font
38 | func Font(_ size: CGFloat) -> UIFont {
39 | return UIFont(name: "ArialMT", size: size)!
40 | }
41 |
--------------------------------------------------------------------------------
/Files/Zip/ZipFileType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ZipFileType.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/9/7.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Zip
11 | import Toast_Swift
12 |
13 | struct ZipFileType: FileType {
14 | let name = "Zip"
15 | let pathExtensions = ["zip"]
16 |
17 | func thumbnail(file: File, completion: @escaping (UIImage) -> Void) {
18 | completion(UIImage(named: "icon_zip")!)
19 | }
20 |
21 | func openFile(_ file: File, document: Document, controller: DocumentBrowserViewController) {
22 | let destination = document.createFilePath(file.name.deletingPathExtension)
23 |
24 | UIApplication.shared.beginIgnoringInteractionEvents()
25 | controller.view.makeToastActivity(.center)
26 | DispatchQueue.global().async {
27 | do {
28 | try Zip.unzipFile(file.url, destination: destination, overwrite: true, password: nil, progress: nil)
29 | } catch {
30 | DispatchQueue.main.async {
31 | controller.view.makeToast(error.localizedDescription)
32 | }
33 | }
34 | DispatchQueue.main.async {
35 | UIApplication.shared.endIgnoringInteractionEvents()
36 | controller.view.hideToastActivity()
37 | }
38 | }
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Files
2 |
3 | ## 如何运行
4 |
5 | #### 环境
6 | Xcode 10.3
7 | node v12.9.0
8 |
9 | ```bash
10 | # clone repo
11 | $ git clone -b develop https://github.com/cezres/Files.git && cd Files/
12 | $ git submodule update --init
13 |
14 | # install pods
15 | $ pod install
16 |
17 | # build files-web
18 | $ cd files-web/
19 | $ yarn && yarn build
20 |
21 | # build ijkplayer
22 | $ cd ../ijkplayer/
23 | $ ./init-ios.sh
24 | $ cd ios
25 | $ ./compile-ffmpeg.sh clean
26 | $ ./compile-ffmpeg.sh all
27 |
28 | # open Files.xcworkspace with Xcode, build and run the Files target.
29 | ```
30 |
31 | ## 截图
32 |
33 |  |  | 
34 | :-|:-|:-
35 |  |  | 
36 | 
37 |
38 | 
39 | 
40 | 
41 |
--------------------------------------------------------------------------------
/Files/Video/MediaPlayer/MediaPlayerTypes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MediaPlayerTypes.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/26.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import IJKMediaFramework
11 |
12 | extension MediaPlayerView {
13 | struct LoadState: OptionSet {
14 | let rawValue: UInt
15 |
16 | static let unknown = LoadState(rawValue: 0)
17 | static let playable = LoadState(rawValue: 1 << 0)
18 | static let playthroughOK = LoadState(rawValue: 1 << 1)
19 | static let stalled = LoadState(rawValue: 1 << 1)
20 |
21 | static let all: [LoadState] = [.unknown, .playable, .playthroughOK, .stalled]
22 | }
23 |
24 | enum PlaybackState {
25 | case stopped
26 | case playing
27 | case paused
28 | case interrupted
29 | case seekingForward
30 | case seekingBackward
31 |
32 | init(playbackState: IJKMPMoviePlaybackState) {
33 | switch playbackState {
34 | case .stopped:
35 | self = .stopped
36 | case .playing:
37 | self = .playing
38 | case .paused:
39 | self = .paused
40 | case .interrupted:
41 | self = .interrupted
42 | case .seekingForward:
43 | self = .seekingForward
44 | case .seekingBackward:
45 | self = .seekingBackward
46 | @unknown default:
47 | fatalError()
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Files/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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | # Package.resolved
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/Build
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
64 |
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots/**/*.png
68 | fastlane/test_output
69 |
--------------------------------------------------------------------------------
/Files/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 |
--------------------------------------------------------------------------------
/files-web/src/services/request.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosResponse } from 'axios'
2 | import camelcaseKeys from 'camelcase-keys'
3 | import { File } from '../types/File'
4 |
5 | export const baseURL = document.location.origin
6 | // export const baseURL = 'http://192.168.101.13:22333'
7 |
8 | export const axiosIns = axios.create({
9 | baseURL: baseURL,
10 | data: null,
11 | })
12 |
13 | export const toCamelcase = (object: any): T | null => {
14 | console.log(object);
15 |
16 | try {
17 | return JSON.parse(
18 | JSON.stringify(
19 | camelcaseKeys(object, {
20 | deep: true,
21 | }),
22 | ),
23 | ) as T
24 | } catch (error) {
25 | console.error(error)
26 | }
27 | return null
28 | }
29 |
30 |
31 | export const fetchFiles = (directory: string = '') => {
32 | return axiosIns
33 | .get(`/document/files?directory=${directory}`)
34 | .then((res: AxiosResponse) => toCamelcase(res.data))
35 | }
36 |
37 | export const uploadFile = (file: any, directory: string, callback: any) => {
38 | if (file === undefined) {
39 | return
40 | }
41 | console.log(file);
42 |
43 | // upload file
44 | var formdata = new FormData()
45 | formdata.append('file', file)
46 |
47 | var xhr = new XMLHttpRequest()
48 | xhr.open('post', `${baseURL}/upload?directory=${directory}`)
49 | xhr.onreadystatechange = (res) => {
50 | console.log(res);
51 | if (xhr.readyState === 4 && xhr.status === 200) {
52 | console.log('上传成功');
53 | callback()
54 | }
55 | }
56 | xhr.upload.onprogress = (event) => {
57 | if (event.lengthComputable) {
58 | var percent = event.loaded / event.total * 100
59 | console.log(`progress = ${percent}`);
60 | }
61 | }
62 | xhr.send(formdata)
63 | }
64 |
--------------------------------------------------------------------------------
/Files/Music/MusicPlayer/MusicPlayer+RemoteComtrol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicPlayer+RemoteComtrol.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/24.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import MediaPlayer
11 |
12 | extension MusicPlayer {
13 | func configRemoteComtrol() {
14 | MPRemoteCommandCenter.shared().pauseCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
15 | MusicPlayer.shared.pause()
16 | return MPRemoteCommandHandlerStatus.success
17 | }
18 | MPRemoteCommandCenter.shared().playCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
19 | do {
20 | try MusicPlayer.shared.resume()
21 | return .success
22 | } catch {
23 | return .commandFailed
24 | }
25 | }
26 | MPRemoteCommandCenter.shared().stopCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
27 | MusicPlayer.shared.stop()
28 | return MPRemoteCommandHandlerStatus.success
29 | }
30 |
31 | /// Previous/Next
32 | MPRemoteCommandCenter.shared().nextTrackCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
33 | do {
34 | try MusicPlayer.shared.next()
35 | return .success
36 | } catch {
37 | return .commandFailed
38 | }
39 | }
40 | MPRemoteCommandCenter.shared().previousTrackCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
41 | do {
42 | try MusicPlayer.shared.previous()
43 | return .success
44 | } catch {
45 | return .commandFailed
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Files/Common/StringExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringExtensions.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/9/4.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension String {
12 | var pathExtension: String {
13 | let array = components(separatedBy: ".")
14 | guard array.count > 1 else {
15 | return ""
16 | }
17 | return array[array.count - 1]
18 | }
19 |
20 | var lastPathComponent: String {
21 | let array = components(separatedBy: "/")
22 | return array[array.count - 1]
23 | }
24 |
25 | var deletingPathExtension: String {
26 | guard let range = range(of: ".", options: String.CompareOptions.backwards, range: nil, locale: nil) else {
27 | return self
28 | }
29 | return String(prefix(upTo: range.lowerBound))
30 | }
31 |
32 | var deletingLastPathComponent: String {
33 | return (self as NSString).deletingLastPathComponent
34 | }
35 |
36 | var relativePath: String {
37 | guard let range = range(of: DocumentDirectory.path) else {
38 | return ""
39 | }
40 | return String(suffix(from: range.upperBound))
41 | }
42 | }
43 |
44 | extension String {
45 | public var urlParametersDecode: [String: String] {
46 | guard let string = self.components(separatedBy: "?").last else { return [:] }
47 | var parameters = [String: String]()
48 | string.components(separatedBy: "&").forEach { (keyValue) in
49 | let components = keyValue.components(separatedBy: "=")
50 | guard components.count == 2 else { return }
51 | parameters[components[0]] = components[1].removingPercentEncoding
52 | }
53 | return parameters
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Files/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | NSAppTransportSecurity
24 |
25 | NSAllowsArbitraryLoads
26 |
27 |
28 | UIBackgroundModes
29 |
30 | audio
31 |
32 | UIFileSharingEnabled
33 |
34 | UILaunchStoryboardName
35 | LaunchScreen
36 | UIRequiredDeviceCapabilities
37 |
38 | armv7
39 |
40 | UISupportedInterfaceOrientations
41 |
42 | UIInterfaceOrientationPortrait
43 | UIInterfaceOrientationLandscapeLeft
44 | UIInterfaceOrientationLandscapeRight
45 |
46 | UISupportedInterfaceOrientations~ipad
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationPortraitUpsideDown
50 | UIInterfaceOrientationLandscapeLeft
51 | UIInterfaceOrientationLandscapeRight
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Files/Extensions/UIViewExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewExtensions.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/2.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIView {
12 | // MARK: - origin
13 |
14 | var origin: CGPoint {
15 | get {
16 | return frame.origin
17 | }
18 | set {
19 | frame = CGRect(origin: newValue, size: size)
20 | }
21 | }
22 |
23 | var x: CGFloat {
24 | get {
25 | return origin.x
26 | }
27 | set {
28 | origin = CGPoint(x: newValue, y: y)
29 | }
30 | }
31 |
32 | var y: CGFloat {
33 | get {
34 | return origin.y
35 | }
36 | set {
37 | origin = CGPoint(x: x, y: newValue)
38 | }
39 | }
40 |
41 | // MARK: - Size
42 |
43 | var size: CGSize {
44 | get {
45 | return frame.size
46 | }
47 | set {
48 | frame = CGRect(origin: origin, size: newValue)
49 | }
50 | }
51 |
52 | var width: CGFloat {
53 | get {
54 | return size.width
55 | }
56 | set {
57 | size = CGSize(width: newValue, height: height)
58 | }
59 | }
60 |
61 | var height: CGFloat {
62 | get {
63 | return size.height
64 | }
65 | set {
66 | size = CGSize(width: width, height: newValue)
67 | }
68 | }
69 | }
70 |
71 | extension UIView {
72 | var viewController: UIViewController? {
73 | var response = next
74 | while response != nil {
75 | if (response as? UIViewController) != nil {
76 | break
77 | }
78 | response = response?.next
79 | }
80 | return response as? UIViewController
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/files-web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Files
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Files/Video/MediaPlayer/MediaPlayerProgressView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MediaProgressView.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/28.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MediaPlayerProgressView: UISlider {
12 |
13 | init() {
14 | super.init(frame: .zero)
15 | minimumTrackTintColor = ColorRGB(219, 92, 92)
16 | maximumTrackTintColor = ColorWhite(86)
17 | minimumValue = 0
18 | maximumValue = 1
19 | value = 0
20 |
21 | if let thumbImage = generateThumbImage() {
22 | setThumbImage(thumbImage, for: .normal)
23 | }
24 | }
25 |
26 | required init?(coder aDecoder: NSCoder) {
27 | fatalError("init(coder:) has not been implemented")
28 | }
29 |
30 | override func trackRect(forBounds bounds: CGRect) -> CGRect {
31 | return CGRect(x: 0, y: (bounds.height - 4) / 2, width: bounds.width, height: 4)
32 | }
33 |
34 | func generateThumbImage() -> UIImage? {
35 | let width: CGFloat = 10
36 | UIGraphicsBeginImageContext(CGSize(width: width, height: width))
37 | guard let context = UIGraphicsGetCurrentContext() else { return nil }
38 |
39 | let backgroundColor = UIColor.clear
40 | context.setStrokeColor(backgroundColor.cgColor)
41 | context.setFillColor(backgroundColor.cgColor)
42 | context.addRect(CGRect(x: 0, y: 0, width: width, height: width))
43 | context.drawPath(using: .fillStroke)
44 |
45 | context.setFillColor(UIColor.white.cgColor)
46 | context.addArc(center: CGPoint(x: width/2, y: width/2), radius: width/2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: false)
47 | context.drawPath(using: .fill)
48 |
49 | let thumbImage = UIGraphicsGetImageFromCurrentImageContext()
50 | UIGraphicsEndImageContext()
51 | return thumbImage
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Files/Music/MusicFileType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicFileType.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/21.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AVFoundation
12 | import Toast_Swift
13 |
14 | struct MusicFileType: FileType {
15 | let name = "Music"
16 | let pathExtensions = ["mp3", "wav"]
17 |
18 | func thumbnail(file: File, completion: @escaping (UIImage) -> Void) {
19 | FileThumbnailCache.shared.retrieveImage(identifier: file.identifier, sourceImage: { () -> UIImage? in
20 | return Music.artwork(for: file.url) ?? UIImage(named: "icon_audio")
21 | }) { (_, image) in
22 | completion(image!)
23 | }
24 | }
25 |
26 | func openFile(_ file: File, document: Document, controller: DocumentBrowserViewController) {
27 | let music = Music(url: file.url)
28 | let items = document.contents.filter { type(of: $0.type) == type(of: MusicFileType()) }.map { Music(url: $0.url) }
29 | let index = items.firstIndex(of: music)!
30 |
31 | do {
32 | if MusicPlayer.shared.music == music {
33 | if MusicPlayer.shared.state == .paused {
34 | try MusicPlayer.shared.resume()
35 | } else if MusicPlayer.shared.state == .playing {
36 | MusicPlayer.shared.pause()
37 | } else {
38 | try MusicPlayer.shared.play(items, index: index)
39 | }
40 | } else {
41 | try MusicPlayer.shared.play(items, index: index)
42 | }
43 |
44 | if MusicPlayer.shared.isPlaying {
45 | controller.navigationController?.pushViewController(MusicPlayerViewController(), animated: true)
46 | }
47 | } catch {
48 | controller.view.makeToast(error.localizedDescription)
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Files/Music/View/MusicIndicatorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicIndicatorView.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/22.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import ESTMusicIndicator
11 |
12 | class MusicIndicatorView: UIView {
13 | private var musicIndicator: ESTMusicIndicatorView!
14 |
15 | init() {
16 | super.init(frame: CGRect(x: 0, y: 0, width: 18, height: 18))
17 |
18 | musicIndicator = ESTMusicIndicatorView(frame: CGRect(x: 0, y: 0, width: 18, height: 18))
19 | musicIndicator.hidesWhenStopped = true
20 | addSubview(musicIndicator)
21 | musicIndicator.snp.makeConstraints { (maker) in
22 | maker.edges.equalTo(self)
23 | }
24 |
25 | let tap = UITapGestureRecognizer(target: self, action: #selector(tapMusicIndicator))
26 | addGestureRecognizer(tap)
27 |
28 | NotificationCenter.default.addObserver(self, selector: #selector(handlePlayerStateChangedNotification), name: MusicPlayer.Notification.didChangeState, object: nil)
29 | }
30 |
31 | required init?(coder aDecoder: NSCoder) {
32 | fatalError("init(coder:) has not been implemented")
33 | }
34 |
35 | deinit {
36 | NotificationCenter.default.removeObserver(self)
37 | }
38 |
39 | override func didMoveToWindow() {
40 | handlePlayerStateChangedNotification()
41 | }
42 |
43 | @objc func tapMusicIndicator() {
44 | guard let navigationController = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController else { return }
45 | navigationController.pushViewController(MusicPlayerViewController(), animated: true)
46 | }
47 |
48 | @objc func handlePlayerStateChangedNotification() {
49 | switch MusicPlayer.shared.state {
50 | case .playing:
51 | musicIndicator.state = .playing
52 | case .paused:
53 | musicIndicator.state = .paused
54 | case .stopped:
55 | musicIndicator.state = .stopped
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Files/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "size" : "60x60",
40 | "idiom" : "iphone",
41 | "filename" : "IMG_0316.png",
42 | "scale" : "3x"
43 | },
44 | {
45 | "idiom" : "ipad",
46 | "size" : "20x20",
47 | "scale" : "1x"
48 | },
49 | {
50 | "idiom" : "ipad",
51 | "size" : "20x20",
52 | "scale" : "2x"
53 | },
54 | {
55 | "idiom" : "ipad",
56 | "size" : "29x29",
57 | "scale" : "1x"
58 | },
59 | {
60 | "idiom" : "ipad",
61 | "size" : "29x29",
62 | "scale" : "2x"
63 | },
64 | {
65 | "idiom" : "ipad",
66 | "size" : "40x40",
67 | "scale" : "1x"
68 | },
69 | {
70 | "idiom" : "ipad",
71 | "size" : "40x40",
72 | "scale" : "2x"
73 | },
74 | {
75 | "idiom" : "ipad",
76 | "size" : "76x76",
77 | "scale" : "1x"
78 | },
79 | {
80 | "idiom" : "ipad",
81 | "size" : "76x76",
82 | "scale" : "2x"
83 | },
84 | {
85 | "idiom" : "ipad",
86 | "size" : "83.5x83.5",
87 | "scale" : "2x"
88 | },
89 | {
90 | "idiom" : "ios-marketing",
91 | "size" : "1024x1024",
92 | "scale" : "1x"
93 | }
94 | ],
95 | "info" : {
96 | "version" : 1,
97 | "author" : "xcode"
98 | }
99 | }
--------------------------------------------------------------------------------
/files-web/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/files-web/src/utils/util.ts:
--------------------------------------------------------------------------------
1 |
2 | export const getUrlParams = (location: Location) => {
3 | let params: any = {}
4 | let search = decodeURI(location.search.slice(1))
5 | console.log(`search = ${location.search}`);
6 | while(search.indexOf('=') && search.length) {
7 | const keyIndex = search.indexOf('=')
8 | const key = search.slice(0, keyIndex)
9 | const flagIndex = search.indexOf('&')
10 | const valueIndex = flagIndex === -1 ? search.length : flagIndex
11 | const value = search.slice(keyIndex + 1, valueIndex)
12 | console.log(`key = [${key}] value = [${value}]`);
13 | params[key] = value
14 | search = search.slice(valueIndex)
15 | }
16 | return params
17 | }
18 |
19 | export const parseDataSize = (size: number) => {
20 | if (size < 1024) {
21 | return `${size}B`
22 | } else if (size < 1024 * 1024) {
23 | return `${(size / 1024).toFixed(2)}KB`
24 | } else if (size < 1024 * 1024 * 1024) {
25 | return `${(size / 1024 / 1024).toFixed(2)}MB`
26 | } else {
27 | return `${(size / 1024 / 1024 / 1024).toFixed(2)}GB`
28 | }
29 | }
30 |
31 | export const deletingLastPathComponent = (path: string) => {
32 | const index = path.indexOf('/', 1)
33 | if (index === -1) {
34 | return '/'
35 | } else {
36 | return path.slice(0, index)
37 | }
38 | }
39 |
40 | export const splitDirectoryPath = (directory: string) => {
41 | console.log(directory);
42 |
43 | let paths: {name: string, path: string}[] = []
44 | paths.push({
45 | name: '全部文件',
46 | path: '/'
47 | })
48 |
49 | let offset = 0
50 | let index = directory.indexOf('/', offset)
51 | while (index >= 0) {
52 | let name = directory.slice(offset, index)
53 | if (name.length > 0) {
54 | name = name.replace(/\\/g, '%')
55 |
56 | paths.push({
57 | name: unescape(name),
58 | path: directory.slice(0, index)
59 | })
60 | }
61 | offset = index + 1
62 | index = directory.indexOf('/', offset)
63 | }
64 | if (offset < directory.length) {
65 | let name = directory.slice(offset, directory.length)
66 | if (name.length > 0) {
67 | paths.push({
68 | name: name,
69 | path: directory
70 | })
71 | }
72 | }
73 | return paths
74 | }
75 |
--------------------------------------------------------------------------------
/Files/File/View/DocumentBrowserControlView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DocumentBrowserControlView.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/22.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol DocumentBrowserControlViewEvent {
12 | func newDirectory()
13 | func fileList()
14 | func photoList()
15 | }
16 |
17 | class DocumentBrowserControlView: UIView {
18 | init() {
19 | super.init(frame: CGRect(x: 0, y: 0, width: 375, height: 44))
20 |
21 | let newButton = UIButton(type: .system)
22 | newButton.setTitle("New", for: .normal)
23 | addSubview(newButton)
24 | newButton.snp.makeConstraints { (maker) in
25 | maker.left.equalTo(12)
26 | maker.bottom.equalTo(-10)
27 | maker.width.equalTo(50)
28 | maker.height.equalTo(24)
29 | }
30 | newButton.addTarget(self, action: #selector(newDirectory), for: .touchUpInside)
31 |
32 | let photoButton = UIButton(type: .system)
33 | photoButton.setTitle("Photo", for: .normal)
34 | photoButton.addTarget(self, action: #selector(photoList(button:)), for: .touchUpInside)
35 | addSubview(photoButton)
36 | photoButton.snp.makeConstraints { (maker) in
37 | maker.right.equalTo(-12)
38 | maker.width.equalTo(50)
39 | maker.height.equalTo(24)
40 | maker.bottom.equalTo(-10)
41 | }
42 | }
43 |
44 | required init?(coder aDecoder: NSCoder) {
45 | fatalError("init(coder:) has not been implemented")
46 | }
47 |
48 | @objc func newDirectory() {
49 | guard let event: DocumentBrowserControlViewEvent = eventStrategy() else { return }
50 | event.newDirectory()
51 | }
52 |
53 | @objc func photoList(button: UIButton) {
54 | guard let eventStrategy: DocumentBrowserControlViewEvent = eventStrategy() else { return }
55 | if button.tag == 0 {
56 | eventStrategy.photoList()
57 | button.setTitle("File", for: .normal)
58 | button.tag = 1
59 | } else {
60 | eventStrategy.fileList()
61 | button.setTitle("Photo", for: .normal)
62 | button.tag = 0
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Files/Extensions/UIImageExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImageExtension.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/20.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension UIImage {
13 | func decode() -> UIImage {
14 | guard let cgImage = self.cgImage else { return self }
15 | let width = cgImage.width
16 | let height = cgImage.height
17 | let bitsPerComponent = cgImage.bitsPerComponent
18 | let bytesPerRow = ByteAlignForCoreAnimation(bytesPerRow: width * 4)
19 | let bitmapInfo = CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
20 | let context: CGContext = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo)!
21 | context.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: CGSize(width: width, height: height)))
22 | let inflatedCGImage = context.makeImage()!
23 | return UIImage(cgImage: inflatedCGImage)
24 | }
25 |
26 | private func ByteAlignForCoreAnimation(bytesPerRow: Int) -> Int {
27 | return ((bytesPerRow + (64 - 1)) / 64) * 64
28 | }
29 |
30 | func scale(width: CGFloat) -> UIImage {
31 | let size = CGSize(width: width, height: width / self.size.width * self.size.height)
32 | UIGraphicsBeginImageContext(size)
33 | draw(in: CGRect(origin: CGPoint(x: 0,y: 0), size: size))
34 | let newImage = UIGraphicsGetImageFromCurrentImageContext()
35 | UIGraphicsEndImageContext()
36 | return newImage!
37 | }
38 |
39 | func square() -> UIImage {
40 | let rect: CGRect
41 | if size.width > size.height {
42 | rect = CGRect(x: (size.width-size.height)/2, y: 0, width: size.height, height: size.height)
43 | }
44 | else if size.width < size.height {
45 | rect = CGRect(x: 0, y: (size.height-size.width)/2, width: size.width, height: size.width)
46 | }
47 | else {
48 | return self
49 | }
50 |
51 | if let imageRef = cgImage!.cropping(to: rect) {
52 | return UIImage(cgImage: imageRef)
53 | }
54 | return self
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Files/File/View/FileListFlowLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileListFlowLayout.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/3.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class FileListFlowLayout: UICollectionViewFlowLayout {
12 | weak var delegate: DocumentBrowserFlowLayoutDelegate?
13 | var numberOfColumns: Int = 4
14 |
15 | var isEditing: Bool = false {
16 | didSet {
17 | guard isEditing != oldValue else { return }
18 | collectionView?.tableUpdate(update: .reloadVisible)
19 | }
20 | }
21 |
22 | override init() {
23 | super.init()
24 | scrollDirection = .vertical
25 | sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
26 | minimumInteritemSpacing = 10.0
27 | minimumLineSpacing = 10.0
28 | }
29 |
30 | required init?(coder aDecoder: NSCoder) {
31 | fatalError("init(coder:) has not been implemented")
32 | }
33 |
34 | override func prepare() {
35 | super.prepare()
36 | collectionView?.register(DocumentCollectionViewCell.classForCoder(), forCellWithReuseIdentifier: "File")
37 |
38 | let number = CGFloat(numberOfColumns)
39 | let width = (collectionView!.width-(number+1)*10) / number
40 | itemSize = DocumentCollectionViewCell.itemSize(for: width)
41 | }
42 | }
43 |
44 | extension FileListFlowLayout: DocumentBrowserFlowLayout {
45 | func cellForItem(at indexPath: IndexPath) -> UICollectionViewCell {
46 | guard let collectionView = collectionView else { return UICollectionViewCell() }
47 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "File", for: indexPath) as! DocumentCollectionViewCell
48 | cell.file = delegate?.flowLayout(self, fileForItemAt: indexPath)
49 | cell.isEditing = isEditing
50 | cell.isSelecting = delegate?.flowLayout(self, isSelectedAt: indexPath) ?? false
51 | return cell
52 | }
53 |
54 | func sizeForItem(at indexPath: IndexPath) -> CGSize {
55 | guard let collectionView = collectionView else { return .zero }
56 | let number = CGFloat(numberOfColumns)
57 | let width = (collectionView.width-(number+1)*10) / 4
58 | return DocumentCollectionViewCell.itemSize(for: width)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - CocoaAsyncSocket (7.6.3)
3 | - CocoaHTTPServer (2.3):
4 | - CocoaAsyncSocket
5 | - CocoaLumberjack
6 | - CocoaLumberjack (3.5.3):
7 | - CocoaLumberjack/Core (= 3.5.3)
8 | - CocoaLumberjack/Core (3.5.3)
9 | - DiffableDataSources (0.2.0):
10 | - DifferenceKit/AppKitExtension (~> 1.1)
11 | - DifferenceKit/UIKitExtension (~> 1.1)
12 | - DifferenceKit/Core (1.1.3)
13 | - DifferenceKit/UIKitExtension (1.1.3):
14 | - DifferenceKit/Core
15 | - ESTMusicIndicator (0.2.0)
16 | - FastImageCache (1.5.1)
17 | - SnapKit (5.0.1)
18 | - Toast-Swift (5.0.0)
19 | - WatchFolder (0.1.0)
20 | - Zip (1.1.0)
21 |
22 | DEPENDENCIES:
23 | - CocoaHTTPServer (from `https://github.com/cezres/CocoaHTTPServer.git`, commit `d25f6f1384c4222763260d7f597d4e304c507631`)
24 | - DiffableDataSources
25 | - ESTMusicIndicator
26 | - FastImageCache
27 | - SnapKit
28 | - Toast-Swift
29 | - WatchFolder (from `https://github.com/cezres/WatchFolder.git`, commit `b869c3fc7daba4c5a85e7bd56fa7681b49c0ce54`)
30 | - Zip
31 |
32 | SPEC REPOS:
33 | https://github.com/cocoapods/specs.git:
34 | - CocoaAsyncSocket
35 | - CocoaLumberjack
36 | - DiffableDataSources
37 | - DifferenceKit
38 | - ESTMusicIndicator
39 | - FastImageCache
40 | - SnapKit
41 | - Toast-Swift
42 | - Zip
43 |
44 | EXTERNAL SOURCES:
45 | CocoaHTTPServer:
46 | :commit: d25f6f1384c4222763260d7f597d4e304c507631
47 | :git: https://github.com/cezres/CocoaHTTPServer.git
48 | WatchFolder:
49 | :commit: b869c3fc7daba4c5a85e7bd56fa7681b49c0ce54
50 | :git: https://github.com/cezres/WatchFolder.git
51 |
52 | CHECKOUT OPTIONS:
53 | CocoaHTTPServer:
54 | :commit: d25f6f1384c4222763260d7f597d4e304c507631
55 | :git: https://github.com/cezres/CocoaHTTPServer.git
56 | WatchFolder:
57 | :commit: b869c3fc7daba4c5a85e7bd56fa7681b49c0ce54
58 | :git: https://github.com/cezres/WatchFolder.git
59 |
60 | SPEC CHECKSUMS:
61 | CocoaAsyncSocket: eafaa68a7e0ec99ead0a7b35015e0bf25d2c8987
62 | CocoaHTTPServer: 5624681fc3473d43b18202f635f9b3abb013b530
63 | CocoaLumberjack: 2f44e60eb91c176d471fdba43b9e3eae6a721947
64 | DiffableDataSources: d67c7fe103c0eb2077fbaa73d9a231721455988e
65 | DifferenceKit: 5018791b6c1fc839921a3c171a0a539ace6ea60c
66 | ESTMusicIndicator: 80a123b51a7de3caa44dc91829b9619185dad0f6
67 | FastImageCache: 2e7a6f677b15336eb3d1a4092a494676256c891d
68 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
69 | Toast-Swift: 36f4720243c8582c2be243a60a51e193c761f541
70 | WatchFolder: 7b59d3707f746b90eda1705ee36c0422ea4c7534
71 | Zip: 8877eede3dda76bcac281225c20e71c25270774c
72 |
73 | PODFILE CHECKSUM: 6b21de00a3f36fa03065c2c3dcf1ec5f4f4ac414
74 |
75 | COCOAPODS: 1.7.5
76 |
--------------------------------------------------------------------------------
/Files/File/View/DocumentBrowserToolBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DocumentBrowserToolBar.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/5/10.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol DocumentBrowserToolBarDelegate: class {
12 | func toolBar(_ toolBar: DocumentBrowserToolBar, didClickItem item: DocumentBrowserToolBar.ItemType)
13 | }
14 |
15 | class DocumentBrowserToolBar: UIVisualEffectView {
16 | enum ItemType: String, CaseIterable {
17 | case delete = "Delete"
18 | case move = "Move"
19 | case copy = "Copy"
20 | case zip = "Zip"
21 | }
22 |
23 | weak var delegate: DocumentBrowserToolBarDelegate?
24 |
25 | init() {
26 | super.init(effect: UIBlurEffect(style: .light))
27 | setupItems()
28 | layer.shadowColor = UIColor.black.cgColor
29 | layer.shadowOpacity = 0.3
30 | layer.shadowOffset = CGSize(width: 0, height: 2)
31 | }
32 |
33 | required init?(coder aDecoder: NSCoder) {
34 | fatalError("init(coder:) has not been implemented")
35 | }
36 |
37 | @objc func clickItem(button: UIButton) {
38 | guard let itemType = ItemType(rawValue: button.titleLabel?.text ?? "") else {
39 | return
40 | }
41 | delegate?.toolBar(self, didClickItem: itemType)
42 | }
43 |
44 | private var itemContentView: UIView!
45 |
46 | override func layoutSubviews() {
47 | super.layoutSubviews()
48 | itemContentView.snp.updateConstraints { (maker) in
49 | maker.bottom.equalTo(-safeAreaInsets.bottom)
50 | }
51 | }
52 |
53 | func setupItems() {
54 | itemContentView = UIView()
55 | contentView.addSubview(itemContentView)
56 | itemContentView.snp.makeConstraints { (maker) in
57 | maker.left.equalTo(15)
58 | maker.right.equalTo(-15)
59 | maker.top.equalTo(0)
60 | maker.bottom.equalTo(0)
61 | }
62 | ItemType.allCases.forEach {
63 | let button = UIButton(type: .system)
64 | button.setTitle($0.rawValue, for: .normal)
65 | button.addTarget(self, action: #selector(clickItem(button:)), for: .touchUpInside)
66 | itemContentView.addSubview(button)
67 | button.snp.makeConstraints({ (maker) in
68 | if itemContentView.subviews.count >= 2 {
69 | maker.left.equalTo(itemContentView.subviews[itemContentView.subviews.count - 2].snp.right).offset(10)
70 | } else {
71 | maker.left.equalTo(0)
72 | }
73 | maker.top.equalTo(0)
74 | maker.bottom.equalTo(0)
75 | maker.width.equalTo(60)
76 | })
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Files/DataTransfer/FileHTTPServer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileHTTPServer.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/9/15.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CocoaHTTPServer
11 | import SystemConfiguration.CaptiveNetwork
12 |
13 | class FileHTTPServer {
14 | static let sharedInstance = FileHTTPServer()
15 | private let httpServer: HTTPServer
16 | var host: String?
17 |
18 | init() {
19 | httpServer = HTTPServer()
20 | httpServer.setType("_http._tcp.")
21 | httpServer.setConnectionClass(FileHTTPConnection.classForCoder())
22 | }
23 |
24 | func start() {
25 | guard !httpServer.isRunning() else { return }
26 | guard let address = getWiFiAddress() else { return }
27 | do {
28 | httpServer.setPort(22333)
29 | httpServer.setInterface(address)
30 | try httpServer.start()
31 |
32 | host = "\(address):22333"
33 | print(host ?? "")
34 | } catch {
35 | print(error)
36 | }
37 | }
38 | }
39 |
40 | /// WIFI
41 | extension FileHTTPServer {
42 | func getWifiName() -> String? {
43 | guard let wifiInterfaces = CNCopySupportedInterfaces() else { return nil }
44 | let interfaces = CFBridgingRetain(wifiInterfaces) as! Array
45 | guard interfaces.count > 0 else { return nil }
46 | let interfaceName = interfaces[0]
47 | guard let networkInfo = CNCopyCurrentNetworkInfo(interfaceName) else { return nil }
48 | let interfaceData = networkInfo as? Dictionary
49 | return interfaceData![kCNNetworkInfoKeySSID as String] as? String
50 | }
51 |
52 | func getWiFiAddress() -> String? {
53 | var ifaddr : UnsafeMutablePointer? = nil
54 | guard getifaddrs(&ifaddr) == 0 else {
55 | return nil
56 | }
57 | defer {
58 | freeifaddrs(ifaddr)
59 | }
60 | var address: String?
61 | var cursor = ifaddr
62 | while cursor != nil {
63 | if cursor!.pointee.ifa_addr.pointee.sa_family == AF_INET && (cursor!.pointee.ifa_flags & UInt32(IFF_LOOPBACK)) == 0 {
64 | if String(cString: cursor!.pointee.ifa_name) == "en0" {
65 | var addr = cursor!.pointee.ifa_addr.pointee
66 | var hostName = [CChar](repeating: 0, count: Int(NI_MAXHOST))
67 | getnameinfo(&addr, socklen_t(cursor!.pointee.ifa_addr.pointee.sa_len), &hostName, socklen_t(hostName.count), nil, socklen_t(0), NI_NUMERICHOST)
68 | address = String(cString: hostName)
69 | break
70 | }
71 | }
72 | cursor = cursor?.pointee.ifa_next
73 | }
74 | return address
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Files/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | File.register(type: DirectoryFileType())
20 | File.register(type: PhotoFileType())
21 | File.register(type: MusicFileType())
22 | File.register(type: VideoFileType())
23 | File.register(type: ZipFileType())
24 |
25 | FileHTTPServer.sharedInstance.start()
26 | window = UIWindow(frame: UIScreen.main.bounds)
27 | window?.rootViewController = UINavigationController(rootViewController: DocumentBrowserViewController())
28 | window?.makeKeyAndVisible()
29 | return true
30 | }
31 |
32 | func applicationWillResignActive(_ application: UIApplication) {
33 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
34 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
35 | }
36 |
37 | func applicationDidEnterBackground(_ application: UIApplication) {
38 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
39 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
40 | }
41 |
42 | func applicationWillEnterForeground(_ application: UIApplication) {
43 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
44 | }
45 |
46 | func applicationDidBecomeActive(_ application: UIApplication) {
47 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
48 | }
49 |
50 | func applicationWillTerminate(_ application: UIApplication) {
51 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
52 | }
53 |
54 |
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/Files/Music/Music.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Music.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/21.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | class Music {
13 | let url: URL
14 | private(set) lazy var metadata: Metadata = Metadata(url: url)
15 |
16 | init(url: URL) {
17 | self.url = url
18 | }
19 | }
20 |
21 | extension Music {
22 | struct Metadata {
23 | let url: URL
24 | let duration: Double
25 | let song: String
26 | let singer: String
27 | let albumName: String
28 | let artwork: UIImage?
29 |
30 | init(url: URL) {
31 | self.url = url
32 | let asset = AVURLAsset(url: url)
33 | var song: String?
34 | var singer: String?
35 | var albumName: String?
36 | var artwork: UIImage?
37 | asset.availableMetadataFormats.forEach { (format) in
38 | asset.metadata(forFormat: format).forEach({ (metadataItem) in
39 | if metadataItem.commonKey == .commonKeyTitle {
40 | song = metadataItem.value as? String
41 | } else if metadataItem.commonKey == .commonKeyArtist {
42 | singer = metadataItem.value as? String
43 | } else if metadataItem.commonKey == .commonKeyAlbumName {
44 | albumName = metadataItem.value as? String
45 | } else if metadataItem.commonKey == .commonKeyArtwork {
46 | if let data = metadataItem.value as? Data {
47 | artwork = UIImage(data: data)
48 | }
49 | }
50 | })
51 | }
52 |
53 | duration = TimeInterval(asset.duration.value) / TimeInterval(asset.duration.timescale)
54 | self.song = song ?? url.deletingPathExtension().lastPathComponent
55 | self.singer = singer ?? "未知"
56 | self.albumName = albumName ?? "未知"
57 | self.artwork = artwork
58 | }
59 | }
60 | }
61 |
62 | extension Music: Equatable {
63 | static func == (lhs: Music, rhs: Music) -> Bool {
64 | return lhs.url == rhs.url
65 | }
66 | }
67 |
68 | extension Music {
69 | static func artwork(for url: URL) -> UIImage? {
70 | let asset = AVURLAsset(url: url)
71 | guard TimeInterval(asset.duration.value) / TimeInterval(asset.duration.timescale) > 0 else { return nil }
72 | for format in asset.availableMetadataFormats {
73 | for metadataItem in asset.metadata(forFormat: format) {
74 | if metadataItem.commonKey == .commonKeyArtwork {
75 | if let data = metadataItem.value as? Data {
76 | return UIImage(data: data)
77 | }
78 | }
79 | }
80 | }
81 | return nil
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Files/Video/MediaPlayer/MediaPlayerProgressHUD.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MediaPlayerProgressHUD.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/28.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MediaPlayerProgressHUD: UIVisualEffectView {
12 | private static let viewTag = String(describing: classForCoder()).hash
13 |
14 | static func show(with currentTime: TimeInterval, duration: TimeInterval) {
15 | guard let window = UIApplication.shared.keyWindow else { return }
16 |
17 | print(String(describing: classForCoder()))
18 |
19 | var progressHUD = window.viewWithTag(viewTag) as? MediaPlayerProgressHUD
20 | if progressHUD == nil {
21 | progressHUD = MediaPlayerProgressHUD()
22 | progressHUD?.tag = viewTag
23 | window.addSubview(progressHUD!)
24 | progressHUD?.snp.makeConstraints({ (maker) in
25 | maker.width.equalTo(100)
26 | maker.height.equalTo(50)
27 | maker.center.equalTo(window.snp.center)
28 | })
29 | }
30 | progressHUD?.layer.removeAllAnimations()
31 | progressHUD?.alpha = 1
32 |
33 | progressHUD?.progressView.progress = Float(currentTime / duration)
34 | progressHUD?.timeLabel.text = "\(currentTime.formatterToTime()) / \(duration.formatterToTime())"
35 | }
36 |
37 | static func hide() {
38 | guard let progressHUD = UIApplication.shared.keyWindow?.viewWithTag(viewTag) as? MediaPlayerProgressHUD else { return }
39 | UIView.animate(withDuration: 0.3, animations: {
40 | progressHUD.alpha = 0
41 | }) { (_) in
42 | progressHUD.removeFromSuperview()
43 | }
44 | }
45 |
46 | init() {
47 | super.init(effect: UIBlurEffect(style: .dark))
48 | layer.cornerRadius = 6
49 | layer.masksToBounds = true
50 |
51 | contentView.addSubview(timeLabel)
52 | contentView.addSubview(progressView)
53 |
54 | timeLabel.snp.makeConstraints { (maker) in
55 | maker.left.equalTo(0)
56 | maker.right.equalTo(0)
57 | maker.height.equalTo(16)
58 | maker.centerY.equalTo(contentView)
59 | }
60 | progressView.snp.makeConstraints { (maker) in
61 | maker.left.equalTo(10)
62 | maker.right.equalTo(-10)
63 | maker.top.equalTo(timeLabel.snp.bottom).offset(5)
64 | maker.height.equalTo(2)
65 | }
66 | }
67 |
68 | required init?(coder aDecoder: NSCoder) {
69 | fatalError("init(coder:) has not been implemented")
70 | }
71 |
72 | lazy var timeLabel: UILabel = {
73 | let label = UILabel()
74 | label.textColor = .white
75 | label.font = UIFont.systemFont(ofSize: 13)
76 | label.textAlignment = .center
77 | return label
78 | }()
79 |
80 | lazy var progressView: UIProgressView = {
81 | let progressView = UIProgressView()
82 | progressView.progressTintColor = ColorRGB(219, 92, 92)
83 | progressView.trackTintColor = ColorWhite(86)
84 | return progressView
85 | }()
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/Files/Extensions/UIResponderExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIResponderExtensions.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/2.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIResponder {
12 | func eventStrategy() -> T? {
13 | if let eventStrategy: T = self as? T {
14 | return eventStrategy
15 | } else {
16 | return next?.eventStrategy()
17 | }
18 | }
19 | }
20 |
21 | extension UIResponder {
22 | func routerEvent(with eventName: String, userInfo: Any?) {
23 | if let action = eventStrategyDict[eventName] {
24 | action.perform(target: self, userInfo: userInfo)
25 | } else {
26 | next?.routerEvent(with: eventName, userInfo: userInfo)
27 | }
28 | }
29 |
30 | func registerEventStrategy(with eventName: String, action: Selector) {
31 | eventStrategyDict[eventName] = EventStrategy(selector: action)
32 | }
33 |
34 | func registerEventStrategy(with eventName: String, block: @escaping Block) {
35 | eventStrategyDict[eventName] = EventStrategy(block: block)
36 | }
37 |
38 | func registerEventStrategy(with eventName: String, userInfoBlock: @escaping UserInfoBlock) {
39 | eventStrategyDict[eventName] = EventStrategy(userInfoBlock: userInfoBlock)
40 | }
41 | }
42 |
43 | extension UIResponder {
44 | typealias Block = () -> Void
45 | typealias UserInfoBlock = (_ userInfo: Any?) -> Void
46 |
47 | private struct EventStrategy {
48 | let selector: Selector?
49 | let block: Block?
50 | let userInfoBlock: UserInfoBlock?
51 |
52 | init(selector: Selector? = nil, block: Block? = nil, userInfoBlock: UserInfoBlock? = nil) {
53 | self.selector = selector
54 | self.block = block
55 | self.userInfoBlock = userInfoBlock
56 | }
57 |
58 | func perform(target: NSObject, userInfo: Any?) {
59 | if let selector = selector {
60 | if NSStringFromSelector(selector).hasSuffix(":") {
61 | target.perform(selector, with: userInfo)
62 | } else {
63 | target.perform(selector)
64 | }
65 | } else if let block = block {
66 | block()
67 | } else if let userInfoBlock = userInfoBlock {
68 | userInfoBlock(userInfo)
69 | }
70 | }
71 | }
72 |
73 | static private var eventStrategyAssiciationKey: Int = 0
74 |
75 | private var eventStrategyDict: [String: EventStrategy] {
76 | get {
77 | var dict = objc_getAssociatedObject(self, &UIResponder.eventStrategyAssiciationKey) as? [String: EventStrategy]
78 | if dict == nil {
79 | dict = [String: EventStrategy]()
80 | objc_setAssociatedObject(self, &UIResponder.eventStrategyAssiciationKey, dict, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
81 | }
82 | return dict!
83 | }
84 | set {
85 | objc_setAssociatedObject(self, &UIResponder.eventStrategyAssiciationKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Files/File/View/DocumentCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DocumentCollectionViewCell.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SnapKit
11 |
12 | class DocumentCollectionViewCell: UICollectionViewCell {
13 | var file: File! {
14 | didSet {
15 | iconImageView.image = nil
16 | nameLabel.text = file.name
17 | file.thumbnail { [weak self](file, result) in
18 | guard let self = self else { return }
19 | guard self.file == file else { return }
20 | self.iconImageView.image = result
21 | }
22 | }
23 | }
24 |
25 | var isEditing = false {
26 | didSet {
27 | chooseView.isHidden = !isEditing
28 | }
29 | }
30 |
31 | var isSelecting = false {
32 | didSet {
33 | if isSelecting {
34 | chooseView.image = UIImage(named: "icon_choose_y")
35 | } else {
36 | chooseView.image = UIImage(named: "icon_choose_n")
37 | }
38 | }
39 | }
40 |
41 | static func itemSize(for width: CGFloat) -> CGSize {
42 | return CGSize(width: width, height: width + UIFont.systemFont(ofSize: 12).lineHeight * 2)
43 | }
44 |
45 | override init(frame: CGRect) {
46 | super.init(frame: frame)
47 | backgroundView = UIView()
48 | backgroundView?.backgroundColor = UIColor.white
49 | selectedBackgroundView = UIView()
50 | selectedBackgroundView?.backgroundColor = ColorWhite(220)
51 | setupUI()
52 | }
53 |
54 | required init?(coder aDecoder: NSCoder) {
55 | fatalError("init(coder:) has not been implemented")
56 | }
57 |
58 | // MARK: - View
59 | func setupUI() {
60 | contentView.addSubview(nameLabel)
61 | nameLabel.snp.makeConstraints { (make) in
62 | make.left.equalTo(0)
63 | make.right.equalTo(0)
64 | make.bottom.equalTo(0)
65 | make.height.equalTo(nameLabel.font.lineHeight*2)
66 | }
67 |
68 | contentView.addSubview(iconImageView)
69 | iconImageView.snp.makeConstraints { (make) in
70 | make.left.equalTo(10)
71 | make.right.equalTo(-10)
72 | make.height.equalTo(self.snp.width).offset(-20)
73 | make.top.equalTo(10)
74 | }
75 |
76 | contentView.addSubview(chooseView)
77 | chooseView.snp.makeConstraints({ (make) in
78 | make.size.equalTo(CGSize(width: 32, height: 32))
79 | make.top.equalTo(0)
80 | make.right.equalTo(0)
81 | })
82 | }
83 |
84 | private lazy var iconImageView: UIImageView = {
85 | let imageView = UIImageView()
86 | imageView.contentMode = .scaleAspectFill
87 | imageView.isOpaque = true
88 | imageView.clipsToBounds = true
89 | return imageView
90 | }()
91 |
92 | private lazy var nameLabel: UILabel = {
93 | let label = UILabel()
94 | label.isOpaque = true
95 | label.font = UIFont.systemFont(ofSize: 12)
96 | label.textAlignment = .center
97 | label.numberOfLines = 2
98 | label.lineBreakMode = .byTruncatingMiddle
99 | return label
100 | }()
101 |
102 | private lazy var chooseView: UIImageView = {
103 | let imageView = UIImageView()
104 | imageView.image = UIImage(named: "icon_choose_n")
105 | imageView.isHidden = true
106 | return imageView
107 | }()
108 | }
109 |
--------------------------------------------------------------------------------
/Files/Music/Spectrum/SpectrumView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpectrumView.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/16.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | /// Adapted from https://github.com/potato04/AudioSpectrum
10 | import UIKit
11 |
12 | class SpectrumView: UIView {
13 | var barWidth: CGFloat = 3.0
14 | var space: CGFloat = 1.0
15 |
16 | private let bottomSpace: CGFloat = 0.0
17 | private let topSpace: CGFloat = 0.0
18 |
19 | var leftGradientLayer = CAGradientLayer()
20 | var rightGradientLayer = CAGradientLayer()
21 |
22 | var spectra:[[Float]]? {
23 | didSet {
24 | if let spectra = spectra {
25 | // left channel
26 | let leftPath = UIBezierPath()
27 | for (i, amplitude) in spectra[0].enumerated() {
28 | let x = CGFloat(i) * (barWidth + space) + space
29 | let y = translateAmplitudeToYPosition(amplitude: amplitude)
30 | let bar = UIBezierPath(rect: CGRect(x: x, y: y, width: barWidth, height: bounds.height - bottomSpace - y))
31 | leftPath.append(bar)
32 | }
33 | let leftMaskLayer = CAShapeLayer()
34 | leftMaskLayer.path = leftPath.cgPath
35 | leftGradientLayer.frame = CGRect(x: 0, y: topSpace, width: bounds.width, height: bounds.height - topSpace - bottomSpace)
36 | leftGradientLayer.mask = leftMaskLayer
37 |
38 | // right channel
39 | if spectra.count >= 2 {
40 | let rightPath = UIBezierPath()
41 | for (i, amplitude) in spectra[1].enumerated() {
42 | let x = CGFloat(spectra[1].count - 1 - i) * (barWidth + space) + space
43 | let y = translateAmplitudeToYPosition(amplitude: amplitude)
44 | let bar = UIBezierPath(rect: CGRect(x: x, y: y, width: barWidth, height: bounds.height - bottomSpace - y))
45 | rightPath.append(bar)
46 | }
47 | let rightMaskLayer = CAShapeLayer()
48 | rightMaskLayer.path = rightPath.cgPath
49 | rightGradientLayer.frame = CGRect(x: 0, y: topSpace, width: bounds.width, height: bounds.height - topSpace - bottomSpace)
50 | rightGradientLayer.mask = rightMaskLayer
51 | }
52 | }
53 | }
54 | }
55 |
56 | override init(frame: CGRect) {
57 | super.init(frame: frame)
58 | setupView()
59 | }
60 |
61 | required init?(coder aDecoder: NSCoder) {
62 | super.init(coder: aDecoder)
63 | setupView()
64 | }
65 |
66 | private func setupView() {
67 | rightGradientLayer.colors = [UIColor.init(red: 52/255, green: 232/255, blue: 158/255, alpha: 1.0).cgColor,
68 | UIColor.init(red: 15/255, green: 52/255, blue: 67/255, alpha: 1.0).cgColor]
69 | rightGradientLayer.locations = [0.6, 1.0]
70 | self.layer.addSublayer(rightGradientLayer)
71 |
72 | leftGradientLayer.colors = [UIColor.init(red: 194/255, green: 21/255, blue: 0/255, alpha: 1.0).cgColor,
73 | UIColor.init(red: 255/255, green: 197/255, blue: 0/255, alpha: 1.0).cgColor]
74 | leftGradientLayer.locations = [0.6, 1.0]
75 | self.layer.addSublayer(leftGradientLayer)
76 | }
77 |
78 | private func translateAmplitudeToYPosition(amplitude: Float) -> CGFloat {
79 | let barHeight: CGFloat = CGFloat(amplitude) * (bounds.height - bottomSpace - topSpace)
80 | return bounds.height - bottomSpace - barHeight
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/files-web/src/containers/DocumentBrowser/styled.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const DocumentBrowserPanel = styled.div`
4 | display: flex;
5 | margin: 0px 0px 0px 0px;
6 | width: 100vw;
7 | height: 100vh;
8 | background: rgb(247, 247, 247);
9 |
10 | .content {
11 | display: flex;
12 | width: calc(100% - 80px);
13 | height: calc(100% - 80px);
14 | margin: 40px 40px 40px 40px;
15 | border-radius: 6px;
16 | box-shadow: 2px 2px 6px 0 #dfdfdf;
17 | background-color: #ffffff;
18 | padding: 20px;
19 | flex-direction: column;
20 |
21 | .upload {
22 | background: rgb(73, 169, 248);
23 | width: 90px;
24 | height: 35px;
25 | margin-bottom: 20px;
26 | border-radius: 6px;
27 | cursor: pointer;
28 | .text {
29 | margin-top: 7px;
30 | margin-bottom: 8px;
31 | height: 20px;
32 | color: white;
33 | text-align: center;
34 | }
35 | &:hover {
36 | background: rgb(0, 153, 229);
37 | }
38 | >input {
39 | display: none;
40 | }
41 | }
42 |
43 | .title {
44 | width: 100%;
45 | display: flex;
46 | flex-direction: row;
47 | justify-content: space-between;
48 | font-size: 12px;
49 | color: rgb(140, 140, 140);
50 | .left {
51 | display: flex;
52 | flex-direction: row;
53 | .item {
54 | cursor: pointer;
55 | color: rgb(85, 175, 248);
56 | }
57 | }
58 | .right {
59 | text-align: right;
60 | }
61 | }
62 | .header {
63 | background: rgb(243, 251, 255);
64 | margin-top: 10px;
65 | display: flex;
66 | flex-direction: row;
67 | height: 40px;
68 | padding-top: 10px;
69 | padding-bottom: 10px;
70 | .name {
71 | width: 60%;
72 | }
73 | .size {
74 | width: 20%;
75 | }
76 | .date {
77 | width: 20%;
78 | }
79 | }
80 | .table {
81 | overflow-y: scroll;
82 | margin-bottom: 0px;
83 |
84 | .separation_line {
85 | height: 1px;
86 | background: rgb(242, 246, 253);
87 | }
88 |
89 | .cell {
90 | display: flex;
91 | flex-direction: row;
92 | height: 50px;
93 | cursor: pointer;
94 |
95 | &:hover {
96 | background: rgb(243, 251, 255);
97 | }
98 |
99 | .icon {
100 | width: 40px;
101 | height: 50px;
102 | > img {
103 | margin-top: 10px;
104 | width: 30px;
105 | height: 30px;
106 | }
107 | }
108 | .name {
109 | width: calc(60% - 40px);
110 | margin-top: 15px;
111 | height: 20px;
112 | overflow: hidden;
113 | text-overflow: ellipsis;
114 | white-space: nowrap;
115 | padding-right: 40px;
116 | }
117 | .size {
118 | width: 20%;
119 | margin-top: 15px;
120 | height: 20px;
121 | }
122 | .date {
123 | width: 20%;
124 | margin-top: 15px;
125 | height: 20px;
126 | }
127 | }
128 | }
129 | }
130 | `
131 |
132 | export const DirectoryPathPanel = styled.div`
133 | display: flex;
134 | flex-direction: row;
135 | .item {
136 | display: flex;
137 | flex-direction: row;
138 |
139 | >a {
140 | :visited {
141 | color: rgb(85, 175, 248);
142 | }
143 | }
144 |
145 | .separator {
146 | margin-left: 5px;
147 | margin-right: 5px;
148 | }
149 | }
150 | `
151 |
--------------------------------------------------------------------------------
/Files/Video/MediaPlayer/MediaPlayerViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoPlayerViewController.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/26.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MediaPlayerViewController: UIViewController {
12 | var playerView: MediaPlayerView!
13 | let url: URL
14 |
15 | init(url: URL) {
16 | self.url = url
17 | super.init(nibName: nil, bundle: nil)
18 | }
19 |
20 | required init?(coder aDecoder: NSCoder) {
21 | fatalError("init(coder:) has not been implemented")
22 | }
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 | view.backgroundColor = .black
27 |
28 | playerView = MediaPlayerView()
29 | view.addSubview(playerView)
30 |
31 | topView.contentView.addSubview(backButton)
32 | topView.contentView.addSubview(titleLabel)
33 | view.addSubview(topView)
34 |
35 | backButton.snp.makeConstraints { (maker) in
36 | maker.width.equalTo(30)
37 | maker.height.equalTo(30)
38 | maker.left.equalTo(15)
39 | maker.centerY.equalTo(topView.contentView)
40 | }
41 | titleLabel.snp.makeConstraints { (maker) in
42 | maker.left.equalTo(backButton.snp.right).offset(15)
43 | maker.right.equalTo(-15)
44 | maker.top.equalTo(0)
45 | maker.bottom.equalTo(0)
46 | }
47 |
48 | playerView.registerControl(MediaPlayerControlView())
49 | playerView.registerControl(self)
50 | playerView.registerControl(MediaPlayerGestureRecognizer())
51 |
52 | playerView.play(url)
53 | titleLabel.text = url.lastPathComponent
54 | }
55 |
56 | override func viewWillLayoutSubviews() {
57 | super.viewWillLayoutSubviews()
58 | if view.size != playerView.size {
59 | playerView.frame = CGRect(x: 0, y: 0, width: view.width, height: view.height)
60 | topView.frame = CGRect(x: 0, y: 0, width: view.width, height: 44)
61 | }
62 | }
63 |
64 | override func viewDidDisappear(_ animated: Bool) {
65 | super.viewDidDisappear(animated)
66 | playerView.shutdown()
67 | }
68 |
69 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
70 | return .landscape
71 | }
72 |
73 | @objc func back() {
74 | dismiss(animated: true, completion: nil)
75 | }
76 |
77 | lazy var topView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
78 | lazy var backButton: UIButton = {
79 | let button = UIButton(type: .system)
80 | button.tintColor = .white
81 | button.setImage(UIImage(named: "icon_media_player_back")!, for: .normal)
82 | button.addTarget(self, action: #selector(back), for: .touchUpInside)
83 | return button
84 | }()
85 |
86 | lazy var titleLabel: UILabel = {
87 | let label = UILabel()
88 | label.textColor = .white
89 | return label
90 | }()
91 | }
92 |
93 | extension MediaPlayerViewController: MediaPlayerCtrlAble {
94 | func reset() {
95 | }
96 |
97 | func cleanup() {
98 | }
99 |
100 | var isCanHideCtrlView: Bool {
101 | return true
102 | }
103 |
104 | func setControlViewHidden(_ hidden: Bool) {
105 | topView.layer.removeAllAnimations()
106 | if hidden {
107 | UIView.animate(withDuration: 0.3) {
108 | self.topView.transform = .init(translationX: 0, y: -self.topView.height)
109 | }
110 | } else {
111 | UIView.animate(withDuration: 0.3) {
112 | self.topView.transform = .identity
113 | }
114 | }
115 | }
116 |
117 | func layoutView(for bounds: CGRect) {
118 | }
119 |
120 | func playerDidChangeLoadState(_ loadState: MediaPlayerView.LoadState) {
121 | }
122 |
123 | func playerDidChangePlayBackState(_ backState: MediaPlayerView.PlaybackState) {
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/files-web/src/containers/DocumentBrowser/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Routes } from '../../components/Router'
3 | import { baseURL, fetchFiles, uploadFile } from '../../services/request'
4 | import { File } from '../../types/File'
5 | import { getUrlParams, parseDataSize, splitDirectoryPath } from '../../utils/util'
6 | import { DirectoryPathPanel, DocumentBrowserPanel } from './styled'
7 |
8 | export default (props: any) => {
9 | const [contents, setContents] = useState([] as File[])
10 | let directory = ''
11 | const params = getUrlParams(props.location)
12 | if (params.directory) {
13 | directory = params.directory
14 | }
15 | let paths = splitDirectoryPath(directory)
16 |
17 | useEffect(() => {
18 | fetchFiles(directory).then((res) => {
19 | setContents(res || [])
20 | })
21 | }, [directory])
22 |
23 | const onClickItem = (file: File) => {
24 | if (file.type === 'Directory') {
25 | window.open(`${Routes.DocumentBrowser}?directory=${file.path}`, '_self')
26 | } else if (file.type === 'Photo') {
27 | window.open(`${Routes.Photo}?path=${file.path}`, '_self')
28 | } else if (file.type === 'Video') {
29 | window.open(`${Routes.Video}?path=${file.path}`, '_self')
30 | } else {
31 | window.open(`${baseURL}/document/data/${file.name}?path=${file.path}`, '_blank')
32 | }
33 | }
34 | const onClickUpload = () => {
35 | const element = document.getElementById('upload')!
36 | element.click()
37 | }
38 | const onSelectedFile = () => {
39 | const element = document.getElementById('upload')! as any
40 | const file = element.files[0]
41 | if (file === undefined) {
42 | return
43 | }
44 | console.log(file);
45 |
46 | uploadFile(file, directory, () => {
47 | fetchFiles(directory).then((res) => {
48 | if (res) {
49 | setContents(res)
50 | }
51 | })
52 | })
53 | }
54 |
55 | return (
56 |
57 |
58 |
62 |
63 |
64 | {paths.length > 1 && (
65 |
66 |
返回上一级
67 |
{'|'}
68 |
69 | )}
70 | {paths.map((item, index) => {
71 | return (
72 |
73 |
{item.name}
74 | {index !== paths.length -1 &&
{'>'}
}
75 |
76 | )
77 | })}
78 |
79 |
{`共${contents.length}个文件`}
80 |
81 |
82 |
文件名
83 |
大小
84 |
修改日期
85 |
86 |
87 | {contents.map((file, index) => {
88 | return (
89 |
90 | {index > 0 &&
}
91 |
onClickItem(file)}>
92 |
93 |

94 |
95 |
{file.name}
96 |
{file.size > 0 && (file.type === 'Directory' ? '-' : parseDataSize(file.size))}
97 |
{file.modificationDate > 0 && new Date(file.modificationDate * 1000).toLocaleDateString()}
98 |
99 |
100 | )
101 | })}
102 |
103 |
104 |
105 | )
106 | }
107 |
--------------------------------------------------------------------------------
/Files/Common/TableUpdate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListUpdate.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | struct TableUpdate: Equatable {
12 | struct MoveIndex: Equatable {
13 | let index: Int
14 | let newIndex: Int
15 |
16 | static func ==(lhs: MoveIndex, rhs: MoveIndex) -> Bool {
17 | return lhs.index == rhs.index && lhs.newIndex == rhs.newIndex
18 | }
19 | }
20 |
21 | enum UpdateType {
22 | case insert
23 | case delete
24 | case reload
25 | case move
26 | case reloadAll
27 | case reloadVisible
28 | }
29 |
30 | let type: UpdateType
31 | let indexs: [Int]!
32 | let moveIndexs: [MoveIndex]!
33 |
34 | private init(indexs: [Int]?, moveIndexs: [MoveIndex]?, type: UpdateType) {
35 | self.indexs = indexs ?? []
36 | self.moveIndexs = moveIndexs ?? []
37 | self.type = type
38 | }
39 |
40 | static func ==(lhs: TableUpdate, rhs: TableUpdate) -> Bool {
41 | return lhs.type == rhs.type && lhs.indexs == rhs.indexs && lhs.moveIndexs == rhs.moveIndexs
42 | }
43 | }
44 |
45 | extension TableUpdate {
46 | static func insert(indexs: [Int]) -> TableUpdate {
47 | return TableUpdate(indexs: indexs, moveIndexs: nil, type: .insert)
48 | }
49 | static func delete(indexs: [Int]) -> TableUpdate {
50 | return TableUpdate(indexs: indexs, moveIndexs: nil, type: .delete)
51 | }
52 | static func reload(indexs: [Int]) -> TableUpdate {
53 | return TableUpdate(indexs: indexs, moveIndexs: nil, type: .reload)
54 | }
55 | static func move(moveIndexs: [(Int, Int)]) -> TableUpdate {
56 | let indexs = moveIndexs.map({ MoveIndex(index: $0.0, newIndex: $0.1) })
57 | return TableUpdate(indexs: nil, moveIndexs: indexs, type: .move)
58 | }
59 | static let reloadAll: TableUpdate = TableUpdate(indexs: nil, moveIndexs: nil, type: .reloadAll)
60 | static let reloadVisible: TableUpdate = TableUpdate(indexs: nil, moveIndexs: nil, type: .reloadVisible)
61 | }
62 |
63 | protocol TableUpdateProtocol {
64 | func tableUpdate(update: TableUpdate)
65 | }
66 |
67 | extension TableUpdateProtocol where Self: UICollectionView {
68 | func tableUpdate(update: TableUpdate) {
69 | switch update.type {
70 | case .reloadAll:
71 | reloadData()
72 | case .insert:
73 | insertItems(at: update.indexs.map({ IndexPath(row: $0, section: 0) }))
74 | case .delete:
75 | deleteItems(at: update.indexs.map({ IndexPath(row: $0, section: 0) }))
76 | case .reload:
77 | reloadItems(at: update.indexs.map({ IndexPath(row: $0, section: 0) }))
78 | case .move:
79 | update.moveIndexs.map {(
80 | index: IndexPath(row: $0.index, section: 0),
81 | newIndex: IndexPath(row: $0.newIndex, section: 0)
82 | )}.forEach {
83 | moveItem(at: $0.index, to: $0.newIndex)
84 | }
85 | case .reloadVisible:
86 | reloadItems(at: visibleCells.map({ indexPath(for: $0)! }))
87 | }
88 | }
89 | }
90 |
91 | extension UICollectionView: TableUpdateProtocol {}
92 |
93 | extension TableUpdateProtocol where Self: UITableView {
94 | func tableUpdate(update: TableUpdate) {
95 | switch update.type {
96 | case .reloadAll:
97 | reloadData()
98 | case .insert:
99 | insertRows(at: update.indexs.map({ IndexPath(row: $0, section: 0) }), with: .none)
100 | case .delete:
101 | deleteRows(at: update.indexs.map({ IndexPath(row: $0, section: 0) }), with: .none)
102 | case .reload:
103 | reloadRows(at: update.indexs.map({ IndexPath(row: $0, section: 0) }), with: .none)
104 | case .move:
105 | update.moveIndexs.map {(
106 | index: IndexPath(row: $0.index, section: 0),
107 | newIndex: IndexPath(row: $0.newIndex, section: 0)
108 | )}.forEach {
109 | moveRow(at: $0.index, to: $0.newIndex)
110 | }
111 | case .reloadVisible:
112 | reloadRows(at: visibleCells.map({ indexPath(for: $0)! }), with: .none)
113 | }
114 | }
115 | }
116 |
117 | extension UITableView: TableUpdateProtocol {}
118 |
--------------------------------------------------------------------------------
/Files.xcodeproj/xcshareddata/xcschemes/Files.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
66 |
72 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
90 |
92 |
98 |
99 |
100 |
101 |
103 |
104 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/Files/File/File.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import FastImageCache
11 | import DifferenceKit
12 |
13 | class File {
14 | let url: URL
15 | lazy private(set) var name: String = url.lastPathComponent
16 | lazy private(set) var pathExtension: String = url.pathExtension
17 | lazy private(set) var relativePath: String = {
18 | guard let range = url.path.range(of: DocumentDirectory.path) else { return url.path }
19 | return String(url.path[range.upperBound...])
20 | }()
21 | lazy private(set) var identifier: String = {
22 | let UUIDBytes = FICUUIDBytesFromMD5HashOfString(relativePath)
23 | return FICStringWithUUIDBytes(UUIDBytes)
24 | }()
25 | lazy private(set) var type: FileType = {
26 | var isDirectory: ObjCBool = false
27 | FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
28 | if isDirectory.boolValue {
29 | return DirectoryFileType()
30 | }
31 | return File.types.first { $0.pathExtensions.contains(pathExtension.lowercased()) } ?? UnknownFileType()
32 | }()
33 | lazy private(set) var attributes = Attributes(url: url)
34 |
35 | init(url: URL) {
36 | self.url = url
37 | }
38 |
39 | // MARK: - Thumbnail
40 | func thumbnail(completion: @escaping (File, UIImage) -> Void) {
41 | type.thumbnail(file: self) { [weak self](image) in
42 | guard let self = self else { return }
43 | completion(self, image)
44 | }
45 | }
46 |
47 | // MARK: - Open
48 | func open(document: Document, controller: DocumentBrowserViewController) {
49 | type.openFile(self, document: document, controller: controller)
50 | }
51 |
52 | // MARK: - Types
53 | fileprivate static var types = [FileType]()
54 |
55 | static func register(type: FileType) {
56 | types.append(type)
57 | }
58 | }
59 |
60 | extension File {
61 | struct Attributes {
62 | let url: URL
63 | let attributes: [FileAttributeKey: Any]
64 |
65 | var size: UInt? {
66 | return (attributes[.size] as? NSNumber)?.uintValue
67 | }
68 | var modificationDate: Date? {
69 | return attributes[.modificationDate] as? Date
70 | }
71 |
72 | init(url: URL) {
73 | self.url = url
74 | attributes = (try? FileManager.default.attributesOfItem(atPath: url.path)) ?? [:]
75 | }
76 | }
77 | }
78 |
79 | extension File: Equatable {
80 | static func == (lhs: File, rhs: File) -> Bool {
81 | return lhs.identifier == rhs.identifier
82 | }
83 | }
84 |
85 | extension File: Differentiable {
86 | typealias DifferenceIdentifier = Int
87 |
88 | var differenceIdentifier: File.DifferenceIdentifier {
89 | return url.hashValue
90 | }
91 | }
92 |
93 | extension File: CustomStringConvertible {
94 | var description: String {
95 | return "\(name) \(type.name))"
96 | }
97 | }
98 |
99 | protocol FileType {
100 | var name: String { get }
101 | var sortIndex: Int { get }
102 | var pathExtensions: [String] { get }
103 | func thumbnail(file: File, completion: @escaping (UIImage) -> Void)
104 | func openFile(_ file: File, document: Document, controller: DocumentBrowserViewController)
105 | }
106 |
107 | extension FileType {
108 | var sortIndex: Int {
109 | return File.types.lastIndex(where: { $0.name == self.name }) ?? Int.max
110 | }
111 | }
112 |
113 | struct DirectoryFileType: FileType {
114 | let name = "Directory"
115 | let pathExtensions: [String] = [""]
116 |
117 | func openFile(_ file: File, document: Document, controller: DocumentBrowserViewController) {
118 | let documentBrowser = DocumentBrowserViewController(directory: file.url)
119 | controller.navigationController?.pushViewController(documentBrowser, animated: true)
120 | }
121 |
122 | func thumbnail(file: File, completion: @escaping (UIImage) -> Void) {
123 | completion(UIImage(named: "icon_directory")!)
124 | }
125 | }
126 |
127 | struct UnknownFileType: FileType {
128 | let name = "Unknown"
129 | let pathExtensions: [String] = []
130 |
131 | func openFile(_ file: File, document: Document, controller: DocumentBrowserViewController) {
132 | }
133 |
134 | func thumbnail(file: File, completion: @escaping (UIImage) -> Void) {
135 | completion(UIImage(named: "icon_unknown")!)
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/Files/File/FileThumbnailCache.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileThumbnail.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/20.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import FastImageCache
11 |
12 | class FileThumbnailCache: NSObject {
13 | typealias CompletionBlock = (String, UIImage?) -> Void
14 |
15 | static let shared = FileThumbnailCache()
16 |
17 | private let imageCache: FICImageCache = FICImageCache.shared()
18 | private let formatFamily = "ImageFormatFamily"
19 | private let formatName = "FileIconCacheFormatName"
20 | private let queue = OperationQueue()
21 |
22 | private override init() {
23 | super.init()
24 | let format = FICImageFormat(name: formatName, family: formatFamily, imageSize: CGSize(width: 160, height: 160), style: .style32BitBGR, maximumCount: 200, devices: .phone, protectionMode: .none)!
25 | imageCache.setFormats([format])
26 | imageCache.delegate = self
27 | // imageCache.reset()
28 | queue.maxConcurrentOperationCount = 1
29 | }
30 |
31 | func retrieveImage(identifier: String, sourceImage: @escaping () -> UIImage?, completion: @escaping CompletionBlock) {
32 | let entity = FileThumbnail(identifier: identifier, sourceImage: sourceImage)
33 | let completionBlock: FICImageCacheCompletionBlock = { (entity, _, image) in
34 | guard let entity = entity as? FileThumbnail else { return }
35 | if Thread.isMainThread {
36 | completion(entity.identifier, image)
37 | } else {
38 | DispatchQueue.main.async {
39 | completion(entity.identifier, image)
40 | }
41 | }
42 | }
43 |
44 | queue.addOperation {
45 | if self.imageCache.imageExists(for: entity, withFormatName: self.formatName) {
46 | self.imageCache.retrieveImage(for: entity, withFormatName: self.formatName, completionBlock: completionBlock)
47 | } else {
48 | self.imageCache.retrieveImage(for: entity, withFormatName: self.formatName, completionBlock: completionBlock)
49 | }
50 | }
51 | }
52 | }
53 |
54 | extension FileThumbnailCache: FICImageCacheDelegate {
55 | }
56 |
57 | private class FileThumbnail: NSObject, FICEntity {
58 | var identifier: String
59 | var sourceImage: () -> UIImage?
60 |
61 | init(identifier: String, sourceImage: @escaping () -> UIImage?) {
62 | self.identifier = identifier
63 | self.sourceImage = sourceImage
64 | }
65 |
66 | // MARK: - FICEntity
67 |
68 | @objc(UUID) var uuid: String! {
69 | return self.identifier
70 | }
71 |
72 | var sourceImageUUID: String! {
73 | return self.identifier
74 | }
75 |
76 | func sourceImageURL(withFormatName formatName: String!) -> URL! {
77 | return URL(fileURLWithPath: "/")
78 | }
79 |
80 | func image(for format: FICImageFormat!) -> UIImage! {
81 | return sourceImage()
82 | }
83 |
84 | func drawingBlock(for image: UIImage!, withFormatName formatName: String!) -> FICEntityImageDrawingBlock! {
85 | return { (context: CGContext?, contextSize: CGSize) -> Void in
86 | guard let context = context else {
87 | return
88 | }
89 | let contextBounds = CGRect(origin: .zero, size: contextSize)
90 | // context.clear(contextBounds)
91 | context.setFillColor(UIColor.white.cgColor)
92 | context.fill(contextBounds)
93 |
94 | var drawRect = CGRect.zero
95 | let imageSize = image.size
96 | let contextAspectRatio = contextSize.width / contextSize.height
97 | let imageAspectRatio = imageSize.width / imageSize.height
98 | if contextAspectRatio == imageAspectRatio {
99 | drawRect = contextBounds
100 | } else if contextAspectRatio > imageAspectRatio {
101 | let drawWidth = contextSize.height * imageAspectRatio
102 | drawRect = CGRect(x: (contextSize.width - drawWidth) / 2, y: 0, width: drawWidth, height: contextSize.height)
103 | } else {
104 | let drawHeight = contextSize.width * imageSize.height / imageSize.width
105 | drawRect = CGRect(x: 0, y: (contextSize.height - drawHeight) / 2, width: contextSize.width, height: drawHeight)
106 | }
107 |
108 | UIGraphicsPushContext(context)
109 | image.draw(in: drawRect)
110 | UIGraphicsPopContext()
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Files/File/Document.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Document.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/18.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import WatchFolder
11 | import DifferenceKit
12 | import Zip
13 |
14 | protocol DocumentDelegate: class {
15 | func document(document: Document, contentsDidUpdate changeset: StagedChangeset<[File]>)
16 | }
17 |
18 | class Document: NSObject {
19 | let directory: URL
20 | weak var delegate: DocumentDelegate?
21 | fileprivate(set) var contents = [File]()
22 |
23 | /// filter
24 | typealias Filter = (_ file: File) -> Bool
25 | var filter: Filter?
26 |
27 | /// private
28 | private let watch: WatchFolder
29 | private let queue: DispatchQueue
30 |
31 | init(directory: URL, filter: Filter? = nil) {
32 | self.directory = directory
33 | watch = WatchFolder(url: directory)
34 | queue = DispatchQueue(label: String(describing: Document.self) + directory.hashValue.description)
35 | super.init()
36 | self.filter = filter
37 | watch.delegate = self
38 | try! watch.start()
39 | loadContents()
40 | }
41 |
42 | deinit {
43 | watch.invalidate()
44 | }
45 | }
46 |
47 | /// Operations
48 | extension Document {
49 | func removeItems(_ indexs: [Int]) throws {
50 | try indexs.sorted { $0 > $1 }.forEach {
51 | try FileManager.default.removeItem(at: contents[$0].url)
52 | }
53 | }
54 |
55 | func moveItems(_ indexs: [Int], to directory: URL) throws {
56 | try indexs.sorted { $0 > $1 }.forEach {
57 | let from = contents[$0].url
58 | let to = directory.appendingPathComponent(from.lastPathComponent)
59 | try FileManager.default.moveItem(at: from, to: to)
60 | }
61 | }
62 |
63 | func createItem(name: String) throws {
64 | try FileManager.default.createDirectory(at: directory.appendingPathComponent(name, isDirectory: true), withIntermediateDirectories: false, attributes: nil)
65 | }
66 |
67 | func copyItems(_ indexs: [Int], to directory: URL) throws {
68 | try indexs.forEach {
69 | let from = contents[$0].url
70 | let to = directory.appendingPathComponent(from.lastPathComponent)
71 | try FileManager.default.copyItem(at: from, to: to)
72 | }
73 | }
74 |
75 | func quickZipItems(_ indexs: [Int]) throws {
76 | let urls = indexs.map { contents[$0].url }
77 | try Zip.zipFiles(paths: urls, zipFilePath: createFilePath("归档.zip"), password: nil) { (progress) in
78 | print("quickZipItems: \(progress)")
79 | }
80 | }
81 | }
82 |
83 | extension Document {
84 | /// 防止文件重名
85 | ///
86 | /// - Parameter lastPathComponent: 文件名称
87 | /// - Returns: 不重复的文件路径
88 | func createFilePath(_ lastPathComponent: String) -> URL {
89 | return generateFilePath(name: lastPathComponent.deletingPathExtension, pathExtension: lastPathComponent.pathExtension, directory: directory)
90 | }
91 | }
92 |
93 | /// Load contents
94 | extension Document {
95 | func loadContents() {
96 | print("\(#function) \(directory.lastPathComponent)")
97 | queue.async {
98 | /// load contents
99 | let contents = try! FileManager.default.contentsOfDirectory(atPath: self.directory.path)
100 | var files = contents.map { File(url: self.directory.appendingPathComponent($0)) }
101 |
102 | /// sort
103 | files = files.sorted { $0.type.sortIndex < $1.type.sortIndex }
104 |
105 | /// filter
106 | if let filter = self.filter {
107 | files = files.filter(filter)
108 | }
109 |
110 | /// changeset
111 | let changeset = StagedChangeset(source: self.contents, target: files)
112 |
113 | DispatchQueue.main.sync {
114 | self.contents = files
115 | self.delegate?.document(document: self, contentsDidUpdate: changeset)
116 | }
117 | }
118 | }
119 | }
120 |
121 | /// Document+WatchFolderDelegate
122 | extension Document: WatchFolderDelegate {
123 | func watchFolderNotification(_ folder: WatchFolder) {
124 | print("\(#function) \(directory.lastPathComponent)")
125 | loadContents()
126 | }
127 | }
128 |
129 | /// UICollectionView+Reload
130 | extension UICollectionView {
131 | func reload(using stagedChangeset: StagedChangeset<[File]>, document: Document) {
132 | reload(using: stagedChangeset, interrupt: nil) { document.contents = $0 }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/Files/Music/View/MusicPlayerControlView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicPlayerToolView.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/21.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MusicPlayerControlView: UIView {
12 | var playButton: UIButton!
13 | var nextButton: UIButton!
14 | var prevButton: UIButton!
15 | var playModeButton: UIButton!
16 | var moreButton: UIButton!
17 |
18 | init() {
19 | super.init(frame: CGRect())
20 | backgroundColor = UIColor(white: 0.4, alpha: 0.4)
21 | initSubviews()
22 | handlePlayStateChangedNotification()
23 | NotificationCenter.default.addObserver(self, selector: #selector(MusicPlayerControlView.handlePlayStateChangedNotification), name: MusicPlayer.Notification.didChangeState, object: nil)
24 | }
25 |
26 | deinit {
27 | NotificationCenter.default.removeObserver(self)
28 | }
29 |
30 | required init?(coder aDecoder: NSCoder) {
31 | fatalError("init(coder:) has not been implemented")
32 | }
33 |
34 | @objc func clickPlay() {
35 | if MusicPlayer.shared.state == .playing {
36 | MusicPlayer.shared.pause()
37 | }
38 | else {
39 | makeToastToWindow { try MusicPlayer.shared.resume() }
40 | }
41 | }
42 |
43 | @objc func clickNext() {
44 | makeToastToWindow { try MusicPlayer.shared.next() }
45 | }
46 |
47 | @objc func clickPrev() {
48 | makeToastToWindow { try MusicPlayer.shared.previous() }
49 | }
50 |
51 | @objc func handlePlayStateChangedNotification() {
52 | if MusicPlayer.shared.state == .playing {
53 | playButton.setImage(#imageLiteral(resourceName: "icon-pause"), for: .normal)
54 | }
55 | else if MusicPlayer.shared.state == .paused {
56 | playButton.setImage(#imageLiteral(resourceName: "icon-play"), for: .normal)
57 | }
58 | else if MusicPlayer.shared.state == .stopped {
59 | playButton.setImage(#imageLiteral(resourceName: "icon-play"), for: .normal)
60 | }
61 | }
62 |
63 | func initSubviews() {
64 | playButton = UIButton(type: .system)
65 | playButton.addTarget(self, action: #selector(MusicPlayerControlView.clickPlay), for: .touchUpInside)
66 | playButton.tintColor = UIColor.white
67 | playButton.setImage(#imageLiteral(resourceName: "icon-play"), for: .normal)
68 | addSubview(playButton)
69 | playButton.snp.makeConstraints { (make) in
70 | make.center.equalTo(self)
71 | make.size.equalTo(CGSize(width: 60, height: 60))
72 | }
73 |
74 | nextButton = UIButton(type: .system)
75 | nextButton.addTarget(self, action: #selector(MusicPlayerControlView.clickNext), for: .touchUpInside)
76 | nextButton.tintColor = UIColor.white
77 | nextButton.setImage(UIImage(named: "icon-next"), for: .normal)
78 | addSubview(nextButton)
79 | nextButton.snp.makeConstraints { (make) in
80 | make.centerY.equalTo(self)
81 | make.left.equalTo(playButton.snp.right).offset(15)
82 | make.size.equalTo(CGSize(width: 60, height: 60))
83 | }
84 |
85 | prevButton = UIButton(type: .system)
86 | prevButton.addTarget(self, action: #selector(MusicPlayerControlView.clickPrev), for: .touchUpInside)
87 | prevButton.tintColor = UIColor.white
88 | prevButton.setImage(UIImage(named: "icon-prev"), for: .normal)
89 | addSubview(prevButton)
90 | prevButton.snp.makeConstraints { (make) in
91 | make.centerY.equalTo(self)
92 | make.right.equalTo(playButton.snp.left).offset(-15)
93 | make.size.equalTo(CGSize(width: 60, height: 60))
94 | }
95 |
96 | playModeButton = UIButton(type: .system)
97 | playModeButton.tintColor = UIColor.white
98 | playModeButton.setImage(UIImage(named: "icon_loopAll"), for: .normal)
99 | addSubview(playModeButton)
100 | playModeButton.snp.makeConstraints { (make) in
101 | make.centerY.equalTo(self)
102 | make.left.equalTo(20)
103 | make.size.equalTo(CGSize(width: 40, height: 40))
104 | }
105 |
106 | moreButton = UIButton(type: .system)
107 | moreButton.tintColor = UIColor.white
108 | moreButton.setImage(UIImage(named: "icon_more"), for: .normal)
109 | addSubview(moreButton)
110 | moreButton.snp.makeConstraints { (make) in
111 | make.centerY.equalTo(self)
112 | make.right.equalTo(-20)
113 | make.size.equalTo(CGSize(width: 40, height: 40))
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Files/Video/MediaPlayer/MediaPlayerGestureRecognizer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MediaPlayerGestureRecognizer.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/29.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import MediaPlayer
11 |
12 | class MediaPlayerGestureRecognizer: MediaPlayerCtrlAble {
13 | enum PanGestureMode {
14 | case none
15 | case volume
16 | case brightness
17 | case progress
18 | }
19 |
20 | weak var playerView: MediaPlayerView! = nil {
21 | didSet {
22 | playerView.addGestureRecognizer(tapGestureRecognizer)
23 | playerView.addGestureRecognizer(panGestureRecognizer)
24 | }
25 | }
26 |
27 | var panGestureMode: PanGestureMode = .none
28 | var tempPlayTime: TimeInterval = 0
29 |
30 | private var tapGestureRecognizer: UITapGestureRecognizer!
31 | private var panGestureRecognizer: UIPanGestureRecognizer!
32 |
33 | init() {
34 | tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
35 | panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
36 | }
37 |
38 | // MARK: Event
39 |
40 | @objc func handleTapGesture() {
41 | playerView.isControlHidden = !playerView.isControlHidden
42 | }
43 |
44 | @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
45 | print(gesture)
46 |
47 | let translation = gesture.translation(in: playerView)
48 | let location = gesture.location(in: playerView)
49 | if panGestureMode == .none {
50 | if abs(translation.y) > abs(translation.x) {
51 | if location.x < playerView.width / 2 {
52 | panGestureMode = .brightness
53 | } else {
54 | panGestureMode = .volume
55 | }
56 | } else if abs(translation.x) > abs(translation.y) {
57 | if playerView.playbackState == .playing || playerView.playbackState == .paused {
58 | panGestureMode = .progress
59 | tempPlayTime = playerView.currentPlaybackTime
60 | }
61 | } else {
62 | return
63 | }
64 | print(panGestureMode)
65 | }
66 |
67 | if gesture.state == .changed {
68 | if panGestureMode == .progress {
69 | if playerView.playbackState == .playing || playerView.playbackState == .paused {
70 | let offset = TimeInterval(translation.x / playerView.width * CGFloat(playerView.duration))
71 | let playTime = min(max(tempPlayTime + offset, 0), playerView.duration)
72 | MediaPlayerProgressHUD.show(with: playTime, duration: playerView.duration)
73 | } else {
74 | MediaPlayerProgressHUD.hide()
75 | }
76 | } else if panGestureMode == .brightness || panGestureMode == .volume {
77 | let offset = -translation.y / 200
78 | if panGestureMode == .brightness {
79 | UIScreen.main.brightness += offset
80 | } else if panGestureMode == .volume {
81 | let volumeView = MPVolumeView(frame: CGRect(x: 0, y: 0, width: 320, height: 100))
82 | var volumeSlider: UISlider?
83 | for view in volumeView.subviews {
84 | if let slider = view as? UISlider {
85 | volumeSlider = slider
86 | break
87 | }
88 | }
89 | if let volumeSlider = volumeSlider {
90 | volumeSlider.setValue(volumeSlider.value + Float(offset), animated: false)
91 | }
92 | }
93 | }
94 | } else if gesture.state == .ended {
95 | if panGestureMode == .progress {
96 | let offset = TimeInterval(translation.x / playerView.width * CGFloat(playerView.duration))
97 | let playTime = min(max(tempPlayTime + offset, 0), playerView.duration)
98 | playerView.currentPlaybackTime = playTime
99 | MediaPlayerProgressHUD.hide()
100 | }
101 | panGestureMode = .none
102 | }
103 | }
104 |
105 | // MARK: MediaPlayerCtrlAble
106 |
107 | func reset() {
108 | }
109 |
110 | func cleanup() {
111 | }
112 |
113 | var isCanHideCtrlView: Bool = true
114 |
115 | func setControlViewHidden(_ hidden: Bool) {
116 | }
117 |
118 | func layoutView(for bounds: CGRect) {
119 | }
120 |
121 | func playerDidChangeLoadState(_ loadState: MediaPlayerView.LoadState) {
122 | }
123 |
124 | func playerDidChangePlayBackState(_ backState: MediaPlayerView.PlaybackState) {
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Files/Photo/PhotoBrowserViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoBrowserViewController.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/19.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SnapKit
11 |
12 | class PhotoBrowserViewController: UIViewController {
13 | private var files: [File]
14 | private(set) var index: Int = 0 {
15 | didSet {
16 | title = "\(index+1)/\(files.count)"
17 | }
18 | }
19 | private var prefetchManager = PhotoPrefetchManager()
20 |
21 | init(files: [File], index: Int = 0) {
22 | self.files = files
23 | self.index = index
24 | super.init(nibName: nil, bundle: nil)
25 | title = "\(index+1)/\(files.count)"
26 | }
27 |
28 | required init?(coder aDecoder: NSCoder) {
29 | fatalError("init(coder:) has not been implemented")
30 | }
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 | prefetchManager.prefetch(url: files[index].url)
35 | setupUI()
36 | }
37 |
38 | override func viewWillLayoutSubviews() {
39 | super.viewWillLayoutSubviews()
40 | collectionView.layoutIfNeeded()
41 | collectionView.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: false)
42 | }
43 |
44 | // MARK: - Views
45 |
46 | private var collectionView: UICollectionView!
47 |
48 | func setupUI() {
49 | let flowLayout = UICollectionViewFlowLayout()
50 | flowLayout.scrollDirection = .horizontal
51 | flowLayout.sectionInset = .zero
52 | flowLayout.minimumInteritemSpacing = 0.0
53 | flowLayout.minimumLineSpacing = 0.0
54 | flowLayout.itemSize = .zero
55 | collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: flowLayout)
56 | collectionView.backgroundColor = UIColor.white
57 | collectionView.register(PhotoCollectionViewCell.classForCoder(), forCellWithReuseIdentifier: "Photo")
58 | collectionView.dataSource = self
59 | collectionView.delegate = self
60 | collectionView.prefetchDataSource = self
61 | collectionView.alwaysBounceHorizontal = true
62 | collectionView.isPagingEnabled = true
63 | collectionView.isPrefetchingEnabled = true
64 | view.addSubview(collectionView)
65 | collectionView.snp.makeConstraints { (maker) in
66 | maker.edges.equalTo(UIEdgeInsets.zero)
67 | }
68 | }
69 | }
70 |
71 |
72 | extension PhotoBrowserViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
73 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
74 | return files.count
75 | }
76 |
77 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
78 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Photo", for: indexPath) as! PhotoCollectionViewCell
79 | cell.imageView.image = nil
80 | prefetchManager.requestImage(url: files[indexPath.row].url) { (result) in
81 | cell.imageView.image = result
82 | }
83 | return cell
84 | }
85 |
86 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
87 | return collectionView.bounds.size
88 | }
89 | }
90 |
91 | extension PhotoBrowserViewController: UICollectionViewDataSourcePrefetching {
92 | func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
93 | prefetchManager.prefetch(urls: indexPaths.map({ files[$0.row].url }))
94 | }
95 |
96 | func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
97 | prefetchManager.cancelPrefetching(urls: indexPaths.map({ files[$0.row].url }))
98 | }
99 | }
100 |
101 | extension PhotoBrowserViewController: UIScrollViewDelegate {
102 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
103 | guard index != Int(scrollView.contentOffset.x / scrollView.frame.size.width) else { return }
104 | index = Int(scrollView.contentOffset.x / scrollView.frame.size.width)
105 | }
106 | }
107 |
108 | private class PhotoCollectionViewCell: UICollectionViewCell {
109 | override init(frame: CGRect) {
110 | super.init(frame: frame)
111 | contentView.addSubview(imageView)
112 | imageView.snp.makeConstraints { (maker) in
113 | maker.edges.equalTo(UIEdgeInsets.zero)
114 | }
115 | }
116 |
117 | required init?(coder aDecoder: NSCoder) {
118 | fatalError("init(coder:) has not been implemented")
119 | }
120 |
121 | // MARK: - Views
122 | lazy var imageView: UIImageView = {
123 | let imageView = UIImageView()
124 | imageView.contentMode = .scaleAspectFit
125 | return imageView
126 | }()
127 | }
128 |
--------------------------------------------------------------------------------
/Files/File/DocumentDirectoryPickerViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DocumentDirectoryPickerViewController.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/5/13.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import DifferenceKit
11 |
12 | class DocumentDirectoryPickerViewController: UIViewController {
13 | typealias SelectedBlock = (_ directory: URL) -> Void
14 | typealias CancelBlock = () -> Void
15 |
16 | private var selectedBlock: SelectedBlock?
17 | private var cancelBlock: CancelBlock?
18 | private var document: Document!
19 |
20 | init(directory: URL = DocumentDirectory, selected: SelectedBlock? = nil, cancel: CancelBlock? = nil) {
21 | self.selectedBlock = selected
22 | self.cancelBlock = cancel
23 | super.init(nibName: nil, bundle: nil)
24 | document = Document(directory: directory) { type(of: $0.type) == type(of: DirectoryFileType()) }
25 | }
26 |
27 | required init?(coder aDecoder: NSCoder) {
28 | fatalError("init(coder:) has not been implemented")
29 | }
30 |
31 | override func viewDidLoad() {
32 | super.viewDidLoad()
33 | title = document.directory.lastPathComponent
34 | view.backgroundColor = .white
35 | setupUI()
36 | document.delegate = self
37 | }
38 |
39 | override func viewDidLayoutSubviews() {
40 | super.viewDidLayoutSubviews()
41 | collectionView.snp.updateConstraints { (maker) in
42 | maker.bottom.equalTo(-view.safeAreaInsets.bottom)
43 | }
44 | confirmButton.snp.updateConstraints { (maker) in
45 | maker.bottom.equalTo(-view.safeAreaInsets.bottom - 15)
46 | }
47 | }
48 |
49 | func showIn(_ controller: UIViewController) {
50 | controller.present(UINavigationController(rootViewController: self), animated: true, completion: nil)
51 | }
52 |
53 | @objc func cancel() {
54 | cancelBlock?()
55 | navigationController?.dismiss(animated: true, completion: nil)
56 | }
57 |
58 | @objc func confirm() {
59 | selectedBlock?(document.directory)
60 | navigationController?.dismiss(animated: true, completion: nil)
61 | }
62 |
63 | private var flowLayout: FileListFlowLayout!
64 | private var collectionView: UICollectionView!
65 | private var confirmButton: UIButton!
66 |
67 | func setupUI() {
68 | let cancelBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel))
69 | navigationItem.rightBarButtonItems = [cancelBarButtonItem]
70 |
71 | flowLayout = FileListFlowLayout()
72 | flowLayout.delegate = self
73 |
74 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
75 | collectionView.backgroundColor = .clear
76 | collectionView.delegate = self
77 | collectionView.dataSource = self
78 | collectionView.alwaysBounceVertical = true
79 | view.addSubview(collectionView)
80 | collectionView.snp.makeConstraints { (maker) in
81 | maker.edges.equalTo(view)
82 | }
83 |
84 | confirmButton = UIButton(type: .system)
85 | confirmButton.setTitle("Confirm", for: .normal)
86 | confirmButton.setTitleColor(.white, for: .normal)
87 | confirmButton.backgroundColor = ColorRGB(54, 132, 203)
88 | confirmButton.layer.cornerRadius = 6
89 | confirmButton.addTarget(self, action: #selector(confirm), for: .touchUpInside)
90 | view.addSubview(confirmButton)
91 | confirmButton.snp.makeConstraints { (maker) in
92 | maker.left.equalTo(30)
93 | maker.right.equalTo(-30)
94 | maker.bottom.equalTo(-30)
95 | maker.height.equalTo(44)
96 | }
97 | }
98 | }
99 |
100 | extension DocumentDirectoryPickerViewController: DocumentDelegate {
101 | func document(document: Document, contentsDidUpdate changeset: StagedChangeset<[File]>) {
102 | collectionView.reload(using: changeset, document: document)
103 | }
104 | }
105 |
106 | extension DocumentDirectoryPickerViewController: DocumentBrowserFlowLayoutDelegate {
107 | func flowLayout(_ flowLayout: DocumentBrowserFlowLayout, fileForItemAt indexPath: IndexPath) -> File {
108 | return document.contents[indexPath.row]
109 | }
110 |
111 | func flowLayout(_ flowLayout: DocumentBrowserFlowLayout, isSelectedAt indexPath: IndexPath) -> Bool {
112 | return false
113 | }
114 | }
115 |
116 |
117 | extension DocumentDirectoryPickerViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
118 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
119 | return document.contents.count
120 | }
121 |
122 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
123 | return flowLayout.cellForItem(at: indexPath)
124 | }
125 |
126 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
127 | let controller = DocumentDirectoryPickerViewController(directory: document.contents[indexPath.row].url, selected: selectedBlock, cancel: cancelBlock)
128 | navigationController?.pushViewController(controller, animated: true)
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/Files/Photo/PhotoPrefetchManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoPrefetchManager.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/20.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class PhotoPrefetchManager {
12 | struct CacheItem {
13 | let url: URL
14 | weak var original: UIImage!
15 | var thumbnails: NSHashTable
16 | }
17 |
18 | var maxConcurrentOperationCount = 4
19 | var thumbnailWidth: CGFloat = 400.0
20 |
21 | private let cache: NSCache!
22 | private let thumbnailCache: NSCache!
23 | private let dispatchQueue: DispatchQueue!
24 | private var prefetchOperations = [PrefetchOperation]()
25 | private var prefetchingOperations = [PrefetchOperation]()
26 |
27 | init() {
28 | cache = NSCache()
29 | cache.totalCostLimit = 1000 * 1000 * 20
30 | thumbnailCache = NSCache()
31 | thumbnailCache.totalCostLimit = 1000 * 1000 * 30
32 | dispatchQueue = DispatchQueue(label: "PhotoPrefetchManager")
33 | }
34 |
35 | func requestImage(url: URL, targetSize: RequestTargetSize = .original, completion: @escaping (_ image: UIImage) -> Void) {
36 | prefetch(url: url, targetSize: targetSize, completion: completion)
37 | }
38 |
39 | func cancelImageRequest() {
40 | }
41 |
42 | func prefetch(urls: [URL]) {
43 | urls.forEach({ prefetch(url: $0) })
44 | }
45 |
46 | func prefetch(url: URL, targetSize: RequestTargetSize = .original, completion: ((_ result: UIImage) -> Void)? = nil) {
47 | dispatchQueue.async {
48 | if let image = self.cache.object(forKey: url.path as NSString) as? UIImage {
49 | DispatchQueue.main.async {
50 | completion?(image)
51 | }
52 | // print("缓存中: \(url.lastPathComponent)")
53 | } else if let operation = self.prefetchingOperations.first(where: { $0.url == url }) {
54 | operation.completionBlock = completion
55 | // print("正在加载: \(url.lastPathComponent)")
56 | } else {
57 | // print("队列中: \(url.lastPathComponent)")
58 | let operation = PrefetchOperation(url, targetSize: targetSize)
59 | if let index = self.prefetchOperations.firstIndex(where: { $0 == operation }) {
60 | if completion == nil {
61 | operation.completionBlock = self.prefetchOperations[index].completionBlock
62 | } else {
63 | operation.completionBlock = completion
64 | }
65 | self.prefetchOperations.remove(at: index)
66 | } else {
67 | operation.completionBlock = completion
68 | }
69 | self.prefetchOperations.append(operation)
70 | self.handleOperations()
71 | }
72 | }
73 | }
74 |
75 | func cancelPrefetching(urls: [URL]) {
76 |
77 | }
78 |
79 | private func handleOperations() {
80 | dispatchQueue.async {
81 | guard self.prefetchOperations.count > 0 else { return }
82 | guard self.prefetchingOperations.count < self.maxConcurrentOperationCount else { return }
83 | let operation = self.prefetchOperations.removeLast()
84 | self.prefetchingOperations.append(operation)
85 | DispatchQueue.global().async {
86 | // print("Cacheing \(operation.url.lastPathComponent)")
87 | if let image = UIImage(contentsOfFile: operation.url.path) {
88 | let result: UIImage
89 | switch (operation.targetSize) {
90 | case .original:
91 | result = image.decode()
92 | case .custom(let size):
93 | result = image.scale(width: size.width).decode()
94 | case .thumbnail:
95 | result = image.scale(width: self.thumbnailWidth).decode()
96 | }
97 | self.cache.setObject(result, forKey: operation.url.path as NSString)
98 | if let block = operation.completionBlock {
99 | DispatchQueue.main.async {
100 | // print("加载完成回调: \(operation.url.lastPathComponent)")
101 | block(result)
102 | }
103 | }
104 | }
105 | // print("Cahced \(operation.url.lastPathComponent)")
106 | self.dispatchQueue.async {
107 | self.prefetchingOperations.removeAll(where: { $0 == operation })
108 | self.handleOperations()
109 | }
110 | }
111 | }
112 | }
113 | }
114 |
115 | enum RequestTargetSize {
116 | case original
117 | case thumbnail
118 | case custom(size: CGSize)
119 | }
120 |
121 | private class PrefetchOperation: Equatable, CustomStringConvertible {
122 | let url: URL
123 | let targetSize: RequestTargetSize
124 | var completionBlock: ((_ result: UIImage) -> Void)?
125 |
126 | init(_ url: URL, targetSize: RequestTargetSize) {
127 | self.url = url
128 | self.targetSize = targetSize
129 | }
130 |
131 | func completion(block: @escaping (_ result: UIImage) -> Void) {
132 | self.completionBlock = block
133 | }
134 |
135 | static func == (lhs: PrefetchOperation, rhs: PrefetchOperation) -> Bool {
136 | return lhs.url == rhs.url
137 | }
138 |
139 | var description: String {
140 | return url.lastPathComponent + " \(String(describing: completionBlock))"
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/files-web/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/Files/Music/MusicPlayer/MusicPlayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicPlayer.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/21.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 | import MediaPlayer
12 |
13 | class MusicPlayer {
14 | static let shared = MusicPlayer()
15 | var playMode: PlayMode = .loopAll
16 | private(set) var items = [Music]()
17 | private(set) var music: Music? {
18 | didSet {
19 | DispatchQueue.main.async {
20 | self.configNowPlayingInfoCenter()
21 | NotificationCenter.default.post(name: Notification.didChangeMusic, object: nil)
22 | }
23 | }
24 | }
25 | private(set) var state: State = .stopped {
26 | didSet {
27 | guard state != oldValue else { return }
28 | DispatchQueue.main.async {
29 | self.configNowPlayingInfoCenter()
30 | NotificationCenter.default.post(name: Notification.didChangeState, object: nil)
31 | }
32 | }
33 | }
34 | var currentTime: TimeInterval {
35 | get {
36 | if state == .paused || state == .stopped {
37 | guard let playTime = pausePlayTime else { return 0 }
38 | return Double(playTime.sampleTime + startingFrame) / playTime.sampleRate
39 | }
40 | guard let playTime = player.lastPlayTime else { return 0 }
41 | return min(Double(playTime.sampleTime + startingFrame) / playTime.sampleRate, duration)
42 | }
43 | set {
44 | seek(to: newValue)
45 | }
46 | }
47 | var duration: TimeInterval {
48 | return audioFile?.duration ?? 0
49 | }
50 | var isPlaying: Bool {
51 | return player.isPlaying
52 | }
53 |
54 | /// Private
55 | private let engine = AVAudioEngine()
56 | private let player: AVAudioPlayerNode = AVAudioPlayerNode()
57 | private var audioFile: AVAudioFile?
58 | private var startingFrame: AVAudioFramePosition = 0
59 | private var pausePlayTime: AVAudioTime?
60 | let bufferSize: Int
61 |
62 | init(bufferSize: Int = 2048) {
63 | self.bufferSize = bufferSize
64 | engine.attach(player)
65 | engine.connect(player, to: engine.mainMixerNode, format: nil)
66 | engine.prepare()
67 | try! engine.start()
68 | engine.mainMixerNode.removeTap(onBus: 0)
69 | engine.mainMixerNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(bufferSize), format: nil) { (buffer, when) in
70 | guard self.state == .playing else { return }
71 | buffer.frameLength = AVAudioFrameCount(self.bufferSize)
72 | NotificationCenter.default.post(name: Notification.didReceivePCMBuffer, object: nil, userInfo: ["buffer": buffer])
73 | }
74 | configRemoteComtrol()
75 | }
76 | }
77 |
78 | // MARK: - Control
79 | extension MusicPlayer {
80 | func next() throws {
81 | guard let music = music else { return }
82 | guard let currentIndex = items.firstIndex(of: music) else { return }
83 | let targetIndex = currentIndex == items.count - 1 ? 0 : currentIndex + 1
84 | try play(music: items[targetIndex])
85 | }
86 | func previous() throws {
87 | guard let music = music else { return }
88 | guard let currentIndex = items.firstIndex(of: music) else { return }
89 | let targetIndex = currentIndex == 0 ? items.count - 1 : currentIndex - 1
90 | try play(music: items[targetIndex])
91 | }
92 |
93 | func play(_ music: Music) throws {
94 | try play([music])
95 | }
96 |
97 | func play(_ items: [Music], index: Int = 0) throws {
98 | self.items = items
99 | try play(music: items[index])
100 | }
101 |
102 | func resume() throws {
103 | switch state {
104 | case .playing:
105 | break
106 | case .paused:
107 | player.play()
108 | state = .playing
109 | case .stopped:
110 | if let music = music {
111 | try play(music: music)
112 | }
113 | }
114 | }
115 |
116 | func stop() {
117 | player.stop()
118 | state = .stopped
119 | UIApplication.shared.beginReceivingRemoteControlEvents()
120 | }
121 |
122 | func pause() {
123 | pausePlayTime = player.lastPlayTime
124 | player.pause()
125 | state = .paused
126 | }
127 |
128 |
129 | private func play(music: Music) throws {
130 | if (state != .stopped) {
131 | stop()
132 | }
133 |
134 | try AVAudioSession.sharedInstance().setActive(true)
135 | try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
136 | UIApplication.shared.beginReceivingRemoteControlEvents()
137 |
138 | audioFile = try AVAudioFile(forReading: music.url)
139 | player.scheduleFile(audioFile!, at: nil) { [weak self] in
140 | /// There will be an error when you stop syncing here.
141 | DispatchQueue.main.async {
142 | self?.playerDidFinishPlaying()
143 | }
144 | }
145 | player.play()
146 |
147 | self.music = music
148 | startingFrame = 0
149 | state = .playing
150 | }
151 |
152 | private func seek(to time: TimeInterval) {
153 | guard let audioFile = audioFile else { return }
154 | guard let lastNodeTime = player.lastRenderTime, let playerTime = player.playerTime(forNodeTime: lastNodeTime) else { return }
155 | let sampleRate = playerTime.sampleRate
156 | let newSampleTime = AVAudioFramePosition(sampleRate * time)
157 | let framestoplay = AVAudioFrameCount(audioFile.length - newSampleTime)
158 |
159 | guard framestoplay > 1000 else { return }
160 | player.stop()
161 | player.scheduleSegment(audioFile, startingFrame: newSampleTime, frameCount: framestoplay, at: nil) { [weak self] in
162 | /// There will be an error when you stop syncing here.
163 | DispatchQueue.main.async {
164 | self?.playerDidFinishPlaying()
165 | }
166 | }
167 | player.play()
168 | startingFrame = newSampleTime
169 | }
170 |
171 | private func playerDidFinishPlaying() {
172 | guard duration - currentTime < 2 else { return }
173 | switch (playMode) {
174 | case .loopSingle:
175 | try? resume()
176 | case .loopAll:
177 | try? next()
178 | case .random:
179 | let index = Int(arc4random_uniform(UInt32(items.count)))
180 | try? play(music: items[index])
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/Files/Video/MediaPlayer/MediaPlayerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoPlayer.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/1.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import MediaPlayer
11 | import IJKMediaFramework
12 |
13 | class MediaPlayerView: UIView {
14 | private(set) var url: URL?
15 |
16 | func play(_ url: URL) {
17 | if playbackState == .paused && loadState.isDisjoint(with: .playable) && self.url == url {
18 | play()
19 | } else {
20 | stop()
21 | self.url = url
22 | initPlayer(with: url)
23 | player?.prepareToPlay()
24 | }
25 | }
26 |
27 | func play() {
28 | player?.play()
29 | }
30 |
31 | func pause() {
32 | player?.pause()
33 | }
34 |
35 | func stop() {
36 | player?.view.removeFromSuperview()
37 | player?.shutdown()
38 | player = nil
39 | controls.forEach({ $0.playerDidChangePlayBackState(.stopped) })
40 | }
41 |
42 | func shutdown() {
43 | controls.forEach { $0.cleanup() }
44 | controls.removeAll()
45 | player?.view.removeFromSuperview()
46 | player?.shutdown()
47 | player = nil
48 | }
49 |
50 | private var player: IJKMediaPlayback?
51 |
52 | init() {
53 | super.init(frame: .zero)
54 |
55 | IJKFFMoviePlayerController.setLogLevel(IJKLogLevel(rawValue: 7))
56 | IJKFFMoviePlayerController.setLogReport(false)
57 |
58 | NotificationCenter.default.addObserver(self, selector: #selector(playerPlaybackStateDidChange(_:)), name: .IJKMPMoviePlayerPlaybackStateDidChange, object: nil)
59 | NotificationCenter.default.addObserver(self, selector: #selector(playerLoadStateDidChange(_:)), name: .IJKMPMoviePlayerLoadStateDidChange, object: nil)
60 | }
61 |
62 | required init?(coder aDecoder: NSCoder) {
63 | fatalError("init(coder:) has not been implemented")
64 | }
65 |
66 | deinit {
67 | NotificationCenter.default.removeObserver(self)
68 | }
69 |
70 | private var tempBounds: CGRect = .zero
71 |
72 | override func layoutSubviews() {
73 | super.layoutSubviews()
74 | if tempBounds != bounds {
75 | tempBounds = bounds
76 | player?.view.frame = bounds
77 | controls.forEach({ $0.layoutView(for: bounds) })
78 | }
79 | }
80 |
81 | func initPlayer(with url: URL) {
82 | let MIMEType = MediaPlayerView.MIMEType(with: url) ?? ""
83 | if AVURLAsset.isPlayableExtendedMIMEType(MIMEType) {
84 | player = IJKAVMoviePlayerController(contentURL: url)
85 | } else {
86 | player = IJKFFMoviePlayerController(contentURL: url, with: .byDefault())
87 | }
88 | player?.view.autoresizingMask = AutoresizingMask(arrayLiteral: .flexibleWidth, .flexibleHeight)
89 | player?.scalingMode = .aspectFit
90 | player?.shouldAutoplay = true
91 | insertSubview(player!.view, at: 0)
92 | }
93 |
94 | // MARK: Controls
95 |
96 | private var controls = [MediaPlayerCtrlAble]()
97 | var hideControlTimeinterval: TimeInterval = 6
98 | var isControlHidden = false {
99 | didSet {
100 | guard oldValue != isControlHidden else { return }
101 | if isControlHidden && controls.reduce(true, { $1.isCanHideCtrlView ? $0 : false }) {
102 | controls.forEach { $0.setControlViewHidden(isControlHidden) }
103 | } else {
104 | controls.forEach { $0.setControlViewHidden(isControlHidden) }
105 | }
106 |
107 | if !isControlHidden && hideControlTimeinterval > 0 && player?.isPlaying() ?? false {
108 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(updateControlHideState(_:)), object: NSNumber(value: true))
109 | perform(#selector(updateControlHideState(_:)), with: NSNumber(value: true), afterDelay: hideControlTimeinterval)
110 | }
111 | }
112 | }
113 |
114 | func registerControl(_ control: MediaPlayerCtrlAble) {
115 | var control = control
116 | control.playerView = self
117 | control.reset()
118 | controls.append(control)
119 | }
120 |
121 | @objc func updateControlHideState(_ state: NSNumber) {
122 | isControlHidden = state.boolValue
123 | }
124 |
125 | // MARK: Notification
126 |
127 | @objc func playerPlaybackStateDidChange(_ notification: Notification) {
128 | guard let playbackState = player?.playbackState else { return }
129 | self.playbackState = PlaybackState(playbackState: playbackState)
130 | }
131 |
132 | @objc func playerLoadStateDidChange(_ notification: Notification) {
133 | guard let loadState = player?.loadState else { return }
134 | self.loadState = LoadState(rawValue: loadState.rawValue)
135 | }
136 |
137 | }
138 |
139 | extension MediaPlayerView {
140 | var currentPlaybackTime: TimeInterval {
141 | get {
142 | return player?.currentPlaybackTime ?? 0
143 | }
144 | set {
145 | play()
146 | player?.currentPlaybackTime = newValue
147 | }
148 | }
149 |
150 | var duration: TimeInterval {
151 | return player?.duration ?? 0
152 | }
153 |
154 | private(set) var playbackState: PlaybackState {
155 | get {
156 | return PlaybackState(playbackState: player?.playbackState ?? .stopped)
157 | }
158 | set {
159 | controls.forEach { $0.playerDidChangePlayBackState(newValue) }
160 |
161 | if newValue == .stopped {
162 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(updateControlHideState(_:)), object: NSNumber(value: true))
163 | } else if newValue == .playing {
164 | if !isControlHidden && hideControlTimeinterval > 0 {
165 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(updateControlHideState(_:)), object: NSNumber(value: true))
166 | perform(#selector(updateControlHideState(_:)), with: NSNumber(value: true), afterDelay: hideControlTimeinterval)
167 | }
168 | } else if newValue == .paused {
169 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(updateControlHideState(_:)), object: NSNumber(value: true))
170 | }
171 | }
172 | }
173 |
174 | private(set) var loadState: LoadState {
175 | get {
176 | return LoadState(rawValue: player?.loadState.rawValue ?? 0)
177 | }
178 | set {
179 | controls.forEach { $0.playerDidChangeLoadState(newValue) }
180 | }
181 | }
182 | }
183 |
184 | extension MediaPlayerView {
185 |
186 | }
187 |
--------------------------------------------------------------------------------
/Files/Music/MusicPlayerViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicPlayerViewController.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/3/21.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | class MusicPlayerViewController: UIViewController {
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 | view.backgroundColor = UIColor.white
16 | setupUI()
17 |
18 | NotificationCenter.default.addObserver(self, selector: #selector(handlePlayerStateChangedNotification), name: MusicPlayer.Notification.didChangeState, object: nil)
19 | NotificationCenter.default.addObserver(self, selector: #selector(handlePlayerMusicChangedNotification), name: MusicPlayer.Notification.didChangeMusic, object: nil)
20 | NotificationCenter.default.addObserver(self, selector: #selector(didReceivePCMBuffer(node:)), name: MusicPlayer.Notification.didReceivePCMBuffer, object: nil)
21 |
22 | handlePlayerMusicChangedNotification()
23 | handlePlayerStateChangedNotification()
24 |
25 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissViewController))
26 | view.addGestureRecognizer(tapGestureRecognizer)
27 |
28 | guard let internalTargets = self.navigationController?.interactivePopGestureRecognizer?.value(forKey: "targets") as? NSArray else { return }
29 | guard let internalTarget = internalTargets.lastObject as? NSObject else { return }
30 | guard let target = internalTarget.value(forKey: "target") else { return }
31 | let action = NSSelectorFromString("handleNavigationTransition:")
32 | let panGestureRecognizer = UIPanGestureRecognizer()
33 | panGestureRecognizer.maximumNumberOfTouches = 1
34 | panGestureRecognizer.addTarget(target, action: action)
35 | view.addGestureRecognizer(panGestureRecognizer)
36 | }
37 |
38 | override func viewWillAppear(_ animated: Bool) {
39 | super.viewWillAppear(animated)
40 | // navigationController?.navigationBar.barStyle = .blackOpaque
41 | navigationController?.setNavigationBarHidden(true, animated: true)
42 | }
43 |
44 | override func viewWillDisappear(_ animated: Bool) {
45 | super.viewWillDisappear(animated)
46 | // navigationController?.navigationBar.barStyle = .black
47 | }
48 |
49 | override func viewDidLayoutSubviews() {
50 | super.viewDidLayoutSubviews()
51 | }
52 |
53 | override var preferredStatusBarStyle: UIStatusBarStyle {
54 | return .lightContent
55 | }
56 |
57 | @objc func dismissViewController() {
58 | _ = navigationController?.popViewController(animated: true)
59 | }
60 |
61 | // MARK: - Notification
62 | @objc func handlePlayerStateChangedNotification() {
63 | let state = MusicPlayer.shared.state
64 | if state == .playing {
65 | infoView.start()
66 | } else if state == .paused {
67 | infoView.pause()
68 | } else if state == .stopped {
69 | infoView.stop()
70 | }
71 | }
72 | @objc func handlePlayerMusicChangedNotification() {
73 | backgroundView.image = MusicPlayer.shared.music?.metadata.artwork
74 | artworkView.image = backgroundView.image
75 | }
76 |
77 | @objc func didReceivePCMBuffer(node: Notification) {
78 | guard let buffer = node.userInfo?["buffer"] as? AVAudioPCMBuffer else { return }
79 | DispatchQueue.main.async {
80 | let spectra = self.analyzer.analyse(with: buffer)
81 | self.spectrumView.spectra = spectra
82 | }
83 | }
84 |
85 | // MAKR: - Views
86 |
87 | func setupUI() {
88 | let contentView = UIView()
89 | contentView.addSubview(artworkView)
90 | contentView.addSubview(spectrumView)
91 | contentView.addSubview(infoView)
92 |
93 | view.addSubview(backgroundView)
94 | view.addSubview(contentView)
95 | view.addSubview(controlView)
96 |
97 | backgroundView.snp.makeConstraints({ (maker) in
98 | maker.edges.equalTo(view)
99 | })
100 | controlView.snp.makeConstraints { (make) in
101 | make.left.equalTo(0)
102 | make.right.equalTo(0)
103 | make.bottom.equalTo(0)
104 | make.height.equalTo(100)
105 | }
106 | contentView.snp.makeConstraints { (maker) in
107 | maker.left.equalTo(0)
108 | maker.right.equalTo(0)
109 | maker.centerY.equalTo(view.snp.centerY).offset(20 / 2 - 100 / 2)
110 | maker.bottom.equalTo(spectrumView.snp.bottom)
111 | }
112 |
113 | artworkView.snp.makeConstraints { (make) in
114 | make.width.equalTo(200)
115 | make.height.equalTo(200)
116 | make.centerX.equalTo(contentView.snp.centerX)
117 | make.top.equalTo(contentView.snp.top)
118 | }
119 | spectrumView.snp.makeConstraints { (maker) in
120 | maker.left.equalTo(20)
121 | maker.right.equalTo(-20)
122 | maker.top.equalTo(infoView.snp.bottom)
123 | maker.height.equalTo(100)
124 | }
125 | infoView.snp.makeConstraints { (make) in
126 | make.left.equalTo(20)
127 | make.right.equalTo(-20)
128 | make.top.equalTo(artworkView.snp.bottom).offset(50)
129 | make.height.equalTo(150)
130 | }
131 |
132 | let spectrumViewWidth = view.width - 40
133 | let barSpace = spectrumViewWidth / CGFloat(analyzer.frequencyBands * 3 - 1)
134 | spectrumView.barWidth = barSpace * 2
135 | spectrumView.space = barSpace
136 | }
137 |
138 | lazy var backgroundView: UIImageView = {
139 | let backgroundView = UIImageView()
140 | backgroundView.clipsToBounds = true
141 | backgroundView.contentMode = .scaleAspectFit
142 | let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
143 | backgroundView.addSubview(blurEffectView)
144 | blurEffectView.snp.makeConstraints { (maker) in
145 | maker.edges.equalTo(backgroundView)
146 | }
147 | return backgroundView
148 | }()
149 |
150 | lazy var artworkView: UIImageView = {
151 | let imageView = UIImageView()
152 | imageView.layer.borderColor = UIColor.white.cgColor
153 | imageView.layer.borderWidth = 2
154 | imageView.layer.cornerRadius = 6
155 | imageView.layer.masksToBounds = true
156 | return imageView
157 | }()
158 |
159 | lazy var controlView: MusicPlayerControlView = {
160 | return MusicPlayerControlView()
161 | }()
162 |
163 | lazy var infoView: MusicPlayerInfoView = {
164 | return MusicPlayerInfoView()
165 | }()
166 |
167 | lazy var spectrumView: SpectrumView = SpectrumView()
168 | lazy var analyzer = SpectrumAnalysis(fftSize: MusicPlayer.shared.bufferSize)
169 | }
170 |
--------------------------------------------------------------------------------
/Files/Music/Spectrum/SpectrumAnalysis.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpectrumAnalysis.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/4/16.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | /// Adapted from https://github.com/potato04/AudioSpectrum
10 | import Foundation
11 | import AVFoundation
12 | import Accelerate
13 |
14 | class SpectrumAnalysis: NSObject {
15 | private var fftSize: Int
16 | private lazy var fftSetup = vDSP_create_fftsetup(vDSP_Length(Int(round(log2(Double(fftSize))))), FFTRadix(kFFTRadix2))
17 |
18 | public var frequencyBands: Int = 80 //频带数量
19 | public var startFrequency: Float = 100 //起始频率
20 | public var endFrequency: Float = 18000 //截止频率
21 |
22 | private lazy var bands: [(lowerFrequency: Float, upperFrequency: Float)] = {
23 | var bands = [(lowerFrequency: Float, upperFrequency: Float)]()
24 | //1:根据起止频谱、频带数量确定增长的倍数:2^n
25 | let n = log2(endFrequency/startFrequency) / Float(frequencyBands)
26 | var nextBand: (lowerFrequency: Float, upperFrequency: Float) = (startFrequency, 0)
27 | for i in 1...frequencyBands {
28 | //2:频带的上频点是下频点的2^n倍
29 | let highFrequency = nextBand.lowerFrequency * powf(2, n)
30 | nextBand.upperFrequency = i == frequencyBands ? endFrequency : highFrequency
31 | bands.append(nextBand)
32 | nextBand.lowerFrequency = highFrequency
33 | }
34 | return bands
35 | }()
36 |
37 | private var spectrumBuffer = [[Float]]()
38 | public var spectrumSmooth: Float = 0.5 {
39 | didSet {
40 | spectrumSmooth = max(0.0, spectrumSmooth)
41 | spectrumSmooth = min(1.0, spectrumSmooth)
42 | }
43 | }
44 |
45 | init(fftSize: Int) {
46 | self.fftSize = fftSize
47 | }
48 |
49 | deinit {
50 | vDSP_destroy_fftsetup(fftSetup)
51 | }
52 |
53 | func analyse(with buffer: AVAudioPCMBuffer) -> [[Float]] {
54 | let channelsAmplitudes = fft(buffer)
55 | let aWeights = createFrequencyWeights()
56 | if spectrumBuffer.count == 0 {
57 | for _ in 0..(repeating: 0, count: bands.count))
59 | }
60 | }
61 | for (index, amplitudes) in channelsAmplitudes.enumerated() {
62 | let weightedAmplitudes = amplitudes.enumerated().map {(index, element) in
63 | return element * aWeights[index]
64 | }
65 | var spectrum = bands.map {
66 | findMaxAmplitude(for: $0, in: weightedAmplitudes, with: Float(buffer.format.sampleRate) / Float(self.fftSize)) * 5
67 | }
68 | spectrum = highlightWaveform(spectrum: spectrum)
69 |
70 | let zipped = zip(spectrumBuffer[index], spectrum)
71 | spectrumBuffer[index] = zipped.map { $0.0 * spectrumSmooth + $0.1 * (1 - spectrumSmooth) }
72 | }
73 | return spectrumBuffer
74 | }
75 |
76 | private func fft(_ buffer: AVAudioPCMBuffer) -> [[Float]] {
77 | var amplitudes = [[Float]]()
78 | guard let floatChannelData = buffer.floatChannelData else { return amplitudes }
79 |
80 | //1:抽取buffer中的样本数据
81 | var channels: UnsafePointer> = floatChannelData
82 | let channelCount = Int(buffer.format.channelCount)
83 | let isInterleaved = buffer.format.isInterleaved
84 |
85 | if isInterleaved {
86 | // deinterleave
87 | let interleavedData = UnsafeBufferPointer(start: floatChannelData[0], count: self.fftSize * channelCount)
88 | var channelsTemp : [UnsafeMutablePointer] = []
89 | for i in 0.. Void in
109 | vDSP_ctoz(typeConvertedTransferBuffer, 2, &fftInOut, 1, vDSP_Length(fftSize / 2))
110 | }
111 |
112 | //4:执行FFT
113 | vDSP_fft_zrip(fftSetup!, &fftInOut, 1, vDSP_Length(round(log2(Double(fftSize)))), FFTDirection(FFT_FORWARD));
114 |
115 | //5:调整FFT结果,计算振幅
116 | fftInOut.imagp[0] = 0
117 | let fftNormFactor = Float(1.0 / (Float(fftSize)))
118 | vDSP_vsmul(fftInOut.realp, 1, [fftNormFactor], fftInOut.realp, 1, vDSP_Length(fftSize / 2));
119 | vDSP_vsmul(fftInOut.imagp, 1, [fftNormFactor], fftInOut.imagp, 1, vDSP_Length(fftSize / 2));
120 | var channelAmplitudes = [Float](repeating: 0.0, count: Int(fftSize / 2))
121 | vDSP_zvabs(&fftInOut, 1, &channelAmplitudes, 1, vDSP_Length(fftSize / 2));
122 | channelAmplitudes[0] = channelAmplitudes[0] / 2 //直流分量的振幅需要再除以2
123 | amplitudes.append(channelAmplitudes)
124 | }
125 | return amplitudes
126 | }
127 |
128 | private func findMaxAmplitude(for band:(lowerFrequency: Float, upperFrequency: Float), in amplitudes: [Float], with bandWidth: Float) -> Float {
129 | let startIndex = Int(round(band.lowerFrequency / bandWidth))
130 | let endIndex = min(Int(round(band.upperFrequency / bandWidth)), amplitudes.count - 1)
131 | return amplitudes[startIndex...endIndex].max()!
132 | }
133 |
134 | private func createFrequencyWeights() -> [Float] {
135 | let Δf = 44100.0 / Float(fftSize)
136 | let bins = fftSize / 2
137 | var f = (0.. [Float] {
154 | //1: 定义权重数组,数组中间的5表示自己的权重
155 | // 可以随意修改,个数需要奇数
156 | let weights: [Float] = [1, 2, 3, 5, 3, 2, 1]
157 | let totalWeights = Float(weights.reduce(0, +))
158 | let startIndex = weights.count / 2
159 | //2: 开头几个不参与计算
160 | var averagedSpectrum = Array(spectrum[0.. [(a,x), (b,y), (c,z)]
163 | let zipped = zip(Array(spectrum[i - startIndex...i + startIndex]), weights)
164 | let averaged = zipped.map { $0.0 * $0.1 }.reduce(0, +) / totalWeights
165 | averagedSpectrum.append(averaged)
166 | }
167 | //4:末尾几个不参与计算
168 | averagedSpectrum.append(contentsOf: Array(spectrum.suffix(startIndex)))
169 | return averagedSpectrum
170 | }
171 |
172 |
173 | }
174 |
--------------------------------------------------------------------------------
/Files/DataTransfer/FileHTTPConnection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileHTTPConnection.swift
3 | // Files
4 | //
5 | // Created by 翟泉 on 2019/9/15.
6 | // Copyright © 2019 cezres. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CocoaHTTPServer
11 |
12 | class FileHTTPConnection: HTTPConnection {
13 | override func expectsRequestBody(fromMethod method: String!, atPath path: String!) -> Bool {
14 | if method == "OPTIONS" {
15 | if path.hasPrefix("/upload") {
16 | return true
17 | }
18 | }
19 | if method == "POST" {
20 | if path.hasPrefix("/upload") {
21 | let parameters = path.urlParametersDecode
22 | uploadDirectory = DocumentDirectory.appendingPathComponent(parameters["directory"] ?? "/")
23 |
24 | guard let request = self.request() else { return false }
25 | guard let contentType = request.headerField("Content-Type") else { return false }
26 | guard let paramsSeparator = contentType.range(of: ";")?.lowerBound else { return false }
27 | let index = contentType.distance(from: contentType.startIndex, to: paramsSeparator)
28 | if index == NSNotFound || index >= contentType.count - 1 {
29 | return false
30 | }
31 | let type = contentType.prefix(upTo: paramsSeparator)
32 | if type != "multipart/form-data" {
33 | return false
34 | }
35 | let params = contentType.suffix(from: contentType.index(paramsSeparator, offsetBy: 1)).components(separatedBy: ";")
36 | params.forEach { (param) in
37 | guard let paramsSeparator = param.range(of: "=")?.lowerBound else { return }
38 | let index = param.distance(from: param.startIndex, to: paramsSeparator)
39 | if (index == NSNotFound || index >= param.count - 1) {
40 | return
41 | }
42 | let name = param[param.index(param.startIndex, offsetBy: 1)...param.index(paramsSeparator, offsetBy: -1)]
43 | let value = param.suffix(from: param.index(paramsSeparator, offsetBy: 1))
44 | if name == "boundary" {
45 | request.setHeaderField("boundary", value: String(value))
46 | }
47 | }
48 | if request.headerField("boundary") == nil {
49 | return false
50 | }
51 | return true
52 | }
53 | }
54 | return super.expectsRequestBody(fromMethod: method, atPath: path)
55 | }
56 |
57 | override func supportsMethod(_ method: String!, atPath path: String!) -> Bool {
58 | if method == "POST" {
59 | if path.hasPrefix("/upload") {
60 | return true
61 | }
62 | }
63 | return super.supportsMethod(method, atPath: path)
64 | }
65 |
66 | override func httpResponse(forMethod method: String!, uri path: String!) -> (CocoaHTTPServer.HTTPResponse & NSObjectProtocol)! {
67 | if method == "GET" {
68 | if path.hasPrefix("/document/files") {
69 | let parameters = path.urlParametersDecode
70 | let directory = DocumentDirectory.appendingPathComponent(parameters["directory"] ?? "")
71 | do {
72 | let contents = try FileManager.default.contentsOfDirectory(atPath: directory.path)
73 | let dict = contents.map({ File(url: directory.appendingPathComponent($0)) }).sorted(by: {
74 | $0.type.sortIndex < $1.type.sortIndex
75 | }).map { (file) -> [String: Any] in
76 | return [
77 | "path": file.relativePath,
78 | "icon": "/document/icon?path=\(file.relativePath.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)",
79 | "type": file.type.name,
80 | "name": file.name,
81 | "size": file.attributes.size ?? "",
82 | "modificationDate": file.attributes.modificationDate?.timeIntervalSince1970 ?? ""
83 | ]
84 | }
85 | let json = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
86 | return HTTPDataResponse(data: String(data: json, encoding: .utf8)?.data(using: .unicode))
87 | } catch {
88 | return HTTPDataResponse(data: error.localizedDescription.data(using: .utf8))
89 | }
90 | } else if path.hasPrefix("/document/icon") {
91 | let parameters = path.urlParametersDecode
92 | let filePath = DocumentDirectory.appendingPathComponent(parameters["path"] ?? "/")
93 | let semaphore = DispatchSemaphore(value: 0)
94 | var image: UIImage?
95 | File(url: URL(fileURLWithPath: filePath.path.removingPercentEncoding!)).thumbnail { (_, result) in
96 | image = result
97 | semaphore.signal()
98 | }
99 | semaphore.wait()
100 | if let image = image {
101 | return HTTPDataResponse(data: image.pngData())
102 | } else {
103 | return super.httpResponse(forMethod: method, uri: path)
104 | }
105 | } else if path.hasPrefix("/document/data") {
106 | let parameters = path.urlParametersDecode
107 | let filePath = DocumentDirectory.appendingPathComponent(parameters["path"] ?? "/")
108 | return HTTPAsyncFileResponse(filePath: filePath.path, for: self)
109 | }
110 | /// html
111 | if path == "/" {
112 | let path = Bundle.main.path(forResource: "build/index.html", ofType: nil)
113 | return HTTPFileResponse(filePath: path, for: self)
114 | } else {
115 | let path = Bundle.main.path(forResource: "build/\(path!)", ofType: nil)
116 | if path == nil {
117 | let path = Bundle.main.path(forResource: "build/index.html", ofType: nil)
118 | return HTTPFileResponse(filePath: path, for: self)
119 | }
120 | return HTTPFileResponse(filePath: path, for: self)
121 | }
122 | } else if method == "POST" {
123 | if path.hasPrefix("/upload") {
124 | return HTTPDataResponse(data: nil)
125 | }
126 | }
127 | return super.httpResponse(forMethod: method, uri: path)
128 | }
129 |
130 |
131 | var parser: MultipartFormDataParser?
132 | var storeFile: FileHandle?
133 | var uploadDirectory: URL?
134 | var uploadFilePath: URL?
135 | var filename: String?
136 |
137 | override func prepareForBody(withSize contentLength: UInt64) {
138 | let boundary = self.request()?.headerField("boundary")
139 | parser = MultipartFormDataParser(boundary: boundary, formEncoding: String.Encoding.utf8.rawValue)
140 | parser?.delegate = self
141 | }
142 |
143 | override func processBodyData(_ postDataChunk: Data!) {
144 | parser?.append(postDataChunk)
145 | }
146 | }
147 |
148 |
149 | extension FileHTTPConnection: MultipartFormDataParserDelegate {
150 | func processStartOfPart(with header: MultipartMessageHeader!) {
151 | let disposition = header.fields["Content-Disposition"] as! MultipartMessageHeaderField
152 | guard let filename = disposition.params["filename"] as? String else { return }
153 | if (filename == "") {
154 | return
155 | }
156 | guard let uploadDirectory = uploadDirectory else {
157 | return
158 | }
159 | self.filename = filename
160 | uploadFilePath = uploadDirectory.appendingPathComponent("upload_\(Int(Date().timeIntervalSince1970 * 1000))_\(filename)")
161 | if !FileManager.default.createFile(atPath: uploadFilePath!.path, contents: nil, attributes: nil) {
162 | print("Could not create file at path: \(uploadFilePath!)")
163 | return
164 | }
165 | storeFile = FileHandle(forWritingAtPath: uploadFilePath!.path)
166 | }
167 |
168 | func processContent(_ data: Data!, with header: MultipartMessageHeader!) {
169 | storeFile?.write(data)
170 | }
171 |
172 | func processEndOfPart(with header: MultipartMessageHeader!) {
173 | storeFile?.closeFile()
174 | storeFile = nil
175 | if let uploadFilePath = uploadFilePath, let filename = filename {
176 | let filePath = generateFilePath(name: filename.deletingPathExtension, pathExtension: filename.pathExtension, directory: uploadDirectory!)
177 | try? FileManager.default.moveItem(at: uploadFilePath, to: filePath)
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------