├── 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 | ![](http://111.231.91.212/wp-content/uploads/2019/09/IMG_0859-139x300.png) | ![](http://111.231.91.212/wp-content/uploads/2019/09/IMG_0860-139x300.png) | ![](http://111.231.91.212/wp-content/uploads/2019/09/IMG_0861-139x300.png) 34 | :-|:-|:- 35 | ![](http://111.231.91.212/wp-content/uploads/2019/09/IMG_0863-139x300.png) | ![](http://111.231.91.212/wp-content/uploads/2019/09/IMG_0880-139x300.png) | ![](http://111.231.91.212/wp-content/uploads/2019/09/IMG_0882-139x300.png) 36 | ![](http://111.231.91.212/wp-content/uploads/2019/09/IMG_0878-139x300.png) 37 | 38 | ![](http://111.231.91.212/wp-content/uploads/2019/09/IMG_0877-300x139.png) 39 | ![](http://111.231.91.212/wp-content/uploads/2019/09/屏幕快照2019-08-25下午6.29.11.png) 40 | ![](http://111.231.91.212/wp-content/uploads/2019/09/屏幕快照2019-08-25下午6.27.36.png) 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 |
59 | 60 |
上传文件
61 |
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 | icon 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 | --------------------------------------------------------------------------------