popeyelau / wiki Goto Github PK
View Code? Open in Web Editor NEW📒Wiki for many useful notes, source, commands and snippets.
📒Wiki for many useful notes, source, commands and snippets.
Widget buildItems() {
List<Widget> sectionA = [Text("A")];
List<Widget> sectionB = [Text("B")];
return Column(
children: [
...sectionA,
...sectionB,
],
);
}
Widget buildItems() {
List<Widget> sectionA = [Text("A")];
List<Widget> sectionB = [Text("B")];
bool showSectionB = false;
return Column(
children: [
...sectionA,
if (showSectionB) ...sectionB,
],
);
}
Widget buildItems() {
List<Widget> sectionA = [Text("A")];
List<Widget> sectionB = [Text("B")];
List<int> sectionC = [1, 2, 3, 4, 5];
bool showSectionB = false;
return Column(
children: [
...sectionA,
if (showSectionB) ...sectionB,
for (var number in sectionC)
if (number.isEven) Text(number.toString())
],
);
}
$ sudo vim /Library/LaunchDaemons/com.cloudflare.cloudflared.plist
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.cloudflare.cloudflared</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/cloudflared</string>
<string>--protocol</string>
<string>http2</string>
<string>tunnel</string>
<string>run</string>
<string>--token</string>
<string>TOKEN VALUE </string>
</array>
$ sudo launchctl load /Library/LaunchDaemons/com.cloudflare.cloudflared.plist
$ sudo launchctl start com.cloudflare.cloudflared
extension String {
var hexColor: UIColor {
let hex = trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt32()
Scanner(string: hex).scanHexInt32(&int)
let a, r, g, b: UInt32
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
return .clear
}
return UIColor(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255)
}
}
https://docs.shorebird.dev/status
https://github.com/shorebirdtech
shorebird.yaml
app_id: xxxxxxxxxxxxxx
flavors:
internal: xxxxxxxxxxxx
stable: xxxxxxxxxxxx
base_url: https://api.shorebird.dev
auto_update: false
// init shorebird
https://github.com/shorebirdtech/engine/blob/shorebird/dev/shell/common/shorebird.cc#L42
https://github.com/shorebirdtech/engine/blob/shorebird/dev/shell/common/shorebird.cc#L73
// init updater
https://github.com/shorebirdtech/updater/blob/main/library/src/c_api.rs#L104
https://github.com/shorebirdtech/updater/blob/main/library/src/updater.rs#L106
// shorebird_start_update_thread();
https://github.com/shorebirdtech/engine/blob/shorebird/dev/shell/common/shorebird.cc#L107
https://github.com/shorebirdtech/updater/blob/main/library/src/updater.rs#L271
https://github.com/shorebirdtech/updater/blob/main/library/src/cache.rs#L316
// libupdater.a
https://github.com/shorebirdtech/engine/blob/shorebird/dev/shell/platform/darwin/ios/BUILD.gn#L202
// settings.application_library_path
https://github.com/shorebirdtech/engine/blob/shorebird/dev/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm#L222
// download patch
https://github.com/shorebirdtech/updater/blob/main/library/src/updater.rs#L206
// restart with shorebird_next_boot_patch_path
https://github.com/shorebirdtech/engine/blob/shorebird/dev/shell/common/shorebird.cc#L96
// Hacks for iOS demo
shorebirdtech/engine@12c4135
https://github.com/shorebirdtech/engine/blob/12c4135b5c90854fb9efc360c0b3ac25d37e9f3f/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm#L229
directory structure
AppData
├── AppDataInfo.plist
├── Documents
├── Library
│ ├── Application Support
│ │ └── shorebird
│ │ └── shorebird_updater
│ │ ├── downloads
│ │ │ ├── 1
│ │ │ └── 2
│ │ ├── slot_0
│ │ │ └── dlc.vmcode
│ │ ├── slot_1
│ │ │ └── dlc.vmcode
│ │ └── state.json
state.json
{
"cache_dir": "/private/var/mobile/Containers/Data/Application/9A5801A8-510F-48FD-A65C-A398F5D64591/Library/Application Support/shorebird/shorebird_updater",
"release_version": "1.0.0+1",
"failed_patches": [],
"successful_patches": [],
"current_boot_slot_index": 1,
"next_boot_slot_index": 1,
"slots": [
{
"patch_number": 1
},
{
"patch_number": 2
}
]
}
get patches
// check patches
curl -X "POST" "https://api.shorebird.dev/api/v1/patches/check" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"platform": "ios",
"arch": "aarch64",
"release_version": "1.0.0+1",
"app_id": "xxxxxxxxxxxxxxxxx",
"channel": "stable",
"patch_number": 1
}'
# 查看设备列表
xcrun simctl list
# 修改模拟器时间等信息(截图用)
xcrun simctl status_bar <device> override --time 12:00 --dataNetwork wifi --wifiMode active --wifiBars 3 --cellularMode active --cellularBars 4 --batteryState charged --batteryLevel 100
# 删除不可用的设备
xcrun simctl delete unavailable
# 卸载应用
xcrun simctl uninstall <device> <app bundle identifier>
# 打开应用
xcrun simctl launch <device> <app bundle identifier>
# 关闭应用
xcrun simctl terminate <device> <app bundle identifier>
参考资料
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/PuerkitoBio/goquery"
)
type VidoeType int
const (
VIDEO_FILM VidoeType = iota + 1
VIDEO_DRAMA
VIDEO_CARTOON
VIDEO_VARIETY
)
type SortType int
func (this SortType) String() string {
switch this {
case SORT_BY_PLAY:
return "play"
case SORT_BY_UPDATE:
return "update"
case SORT_BY_LIKE:
return "like"
default:
return "play"
}
}
const (
SORT_BY_PLAY SortType = iota + 1
SORT_BY_UPDATE
SORT_BY_LIKE
)
const (
HOST string = "https://i.hyys.me"
)
func main() {
home()
video("42768")
videos(VIDEO_FILM, SORT_BY_PLAY, 0)
}
// 首页
func home() {
requestURL := fmt.Sprintf("%v/webIndex.php", HOST)
doc, err := getDocument(requestURL)
if err != nil {
fmt.Println(err)
}
sectionDivs := doc.Find(".hideNavBar>div[class=container]")
sections := make([]*Section, sectionDivs.Length())
sectionDivs.Each(func(i int, s *goquery.Selection) {
section := &Section{}
section.Title = s.Find("p>a:first-of-type").Text()
videoCards := s.Find(".videoContent>div[class=videoCard]")
section.Items = parseVideos(videoCards)
sections[i] = section
})
json, err := marshalIndent(sections)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(*json)
}
// 获取视频详情
func video(id string) {
requestURL := fmt.Sprintf("%v/webIndex.php/?code=videoDetail&vod_id=%v", HOST, id)
doc, err := getDocument(requestURL)
if err != nil {
fmt.Println(err)
return
}
episodeHrefs := doc.Find(".dramaSeriesCont>a")
episodes := make([]*Episode, episodeHrefs.Length())
episodeHrefs.Each(func(i int, s *goquery.Selection) {
episode := &Episode{}
episode.Name = s.Text()
if src, ok := s.Attr("data-href"); ok {
episode.URL = src
}
episodes[i] = episode
})
json, err := marshalIndent(episodes)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(*json)
}
// 获取视频列表
func videos(typeID VidoeType, sort SortType, page int) {
if page < 0 {
page = 0
}
requestURL := fmt.Sprintf("%v/webIndex.php/?code=search&type=%d&most=%s&page=%d", HOST, typeID, sort, page)
doc, err := getDocument(requestURL)
if err != nil {
fmt.Println(err)
return
}
videoCards := doc.Find(".videoContent>div[class=videoCard]")
videos := parseVideos(videoCards)
json, err := marshalIndent(videos)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(*json)
}
// 解析视频 dom
func parseVideos(s *goquery.Selection) []*Video {
videos := make([]*Video, s.Length())
s.Each(func(i int, s *goquery.Selection) {
video := &Video{}
if src, ok := s.Attr("data-video-url"); ok {
video.URL = src
url, _ := (url.ParseRequestURI(src))
video.ID = url.Query().Get("vod_id")
}
if cover, ok := s.Find(".videoCover").Attr("src"); ok {
video.Cover = cover
}
video.Name = s.Find(".videoInfo>p[class=videoName]").Text()
video.Views = s.Find(".videoInfo>div[class=jusBetween]>p").Text()
videos[i] = video
})
return videos
}
// 转换成 json 格式
func marshalIndent(source interface{}) (*string, error) {
bytes, err := json.MarshalIndent(source, "", " ")
if err != nil {
fmt.Println(err)
return nil, err
}
json := string(bytes)
return &json, nil
}
// 获取页面内容
func getDocument(url string) (*goquery.Document, error) {
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromResponse(resp)
if err != nil {
return nil, err
}
return doc, nil
}
type Section struct {
Title string `json:"title"`
Items []*Video `json:"items"`
}
type Video struct {
ID string `json:"id"`
Name string `json:"name"`
URL string `json:"-"` //不输出
Cover string `json:"cover"`
Views string `json:"views"`
}
type Episode struct {
Name string `json:"name"`
URL string `json:"url"`
}
import UIKit
import PlaygroundSupport
protocol Stylable {
init()
}
extension Stylable {
init(style: ViewStyle<Self>) {
self.init()
apply(style)
}
func apply(_ style: ViewStyle<Self>) -> Self {
style.style(self)
return self
}
}
struct ViewStyle<T> {
let style: (T) -> Void
static func +(lhs: ViewStyle<T>, rhs: ViewStyle<T>) -> ViewStyle<T> {
return ViewStyle<T> {
lhs.style($0)
rhs.style($0)
}
}
}
extension UIView: Stylable {}
extension ViewStyle where T: UIButton {
static var filled: ViewStyle<UIButton> {
return ViewStyle<UIButton> {
$0.setTitleColor(.white, for: .normal)
$0.backgroundColor = .red
}
}
static var rounded: ViewStyle<UIButton> {
return ViewStyle<UIButton> {
$0.layer.cornerRadius = 4.0
}
}
static var subtitle: ViewStyle<UIButton> {
return ViewStyle<UIButton> {
$0.titleLabel?.font = .systemFont(ofSize: 12)
$0.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
}
}
static var roundedAndFilled: ViewStyle<UIButton> {
return rounded + filled
}
static func title(_ title: String?, for state: UIControl.State) -> ViewStyle<UIButton> {
return ViewStyle<UIButton> {
$0.setTitle(title, for: state)
$0.sizeToFit()
}
}
}
// Use
let button = UIButton(style: .roundedAndFilled)
.apply(.subtitle)
.apply(.title("Hello World", for: .normal))
PlaygroundPage.current.liveView = button
$ flutter devices
4 connected devices:
SM G8870 (mobile) • 31814575ff1d7ece • android-arm64 • Android 10 (API 29)
iPhone 11 (mobile) • 4B94904E-0460-4D06-93CD-43AA879D5C03 • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-5
(simulator)
iPhone 11 Pro (mobile) • 9E33CF02-924A-4161-B5D4-E6D0AF58217E • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-5
(simulator)
Chrome (web) • chrome • web-javascript • Google Chrome 91.0.4472.114
{
"version": "0.2.0",
"compounds": [{
"name": "Multiple Device Preview",
"request": "launch",
"type": "dart",
"args": [
"--dart-define",
"APP_CHANNEL=Debug",
"--dart-define",
"HTTP_PROXY=172.21.110.101:9999",
"--dart-define",
"DEBUG_ENABLE=true",
"--dart-define",
"API_ENV=0"
],
"configurations": ["iPhone 11", "iPhone 11 Pro", "SM G8870"],
}],
"configurations": [
{
"name": "Debug",
"request": "launch",
"type": "dart",
"args": [
"--dart-define",
"APP_CHANNEL=Debug",
"--dart-define",
"HTTP_PROXY=172.21.110.101:9999",
"--dart-define",
"DEBUG_ENABLE=true",
"--dart-define",
"API_ENV=0"
]
},
{
"name": "iPhone 11",
"request": "launch",
"type": "dart",
"deviceId": "4B94904E-0460-4D06-93CD-43AA879D5C03",
"args": [
"--dart-define",
"APP_CHANNEL=Debug",
"--dart-define",
"HTTP_PROXY=172.21.110.101:9999",
"--dart-define",
"DEBUG_ENABLE=true",
"--dart-define",
"API_ENV=0"
]
},
{
"name": "iPhone 11 Pro",
"request": "launch",
"type": "dart",
"deviceId": "9E33CF02-924A-4161-B5D4-E6D0AF58217E",
"args": [
"--dart-define",
"APP_CHANNEL=Debug",
"--dart-define",
"HTTP_PROXY=172.21.110.101:9999",
"--dart-define",
"DEBUG_ENABLE=true",
"--dart-define",
"API_ENV=0"
]
},
{
"name": "SM G8870",
"request": "launch",
"type": "dart",
"deviceId": "31814575ff1d7ece",
"args": [
"--dart-define",
"APP_CHANNEL=Debug",
"--dart-define",
"HTTP_PROXY=172.21.110.101:9999",
"--dart-define",
"DEBUG_ENABLE=true",
"--dart-define",
"API_ENV=0"
]
},
{
"name": "Pro(profile)",
"request": "launch",
"type": "dart",
"flutterMode": "profile",
"args": [
"--dart-define",
"APP_CHANNEL=Debug",
"--dart-define",
"API_ENV=0",
"--verbose"
]
},
{
"name": "Daily",
"request": "launch",
"type": "dart",
"args": [
"--dart-define",
"APP_CHANNEL=Debug",
"--dart-define",
"API_ENV=2",
"--dart-define",
"HTTP_PROXY=172.21.110.101:9999",
]
},
{
"name": "UAT",
"request": "launch",
"type": "dart",
"args": [
"--dart-define",
"APP_CHANNEL=Debug",
"--dart-define",
"API_ENV=3",
"--dart-define",
"HTTP_PROXY=172.21.110.101:9999",
]
},
],
}
import Foundation
extension Formatter {
static let number = NumberFormatter()
}
extension FloatingPoint {
func fractionDigits(min: Int = 2, max: Int = 2, roundingMode: NumberFormatter.RoundingMode = .down) -> String {
Formatter.number.minimumFractionDigits = min
Formatter.number.maximumFractionDigits = max
Formatter.number.roundingMode = roundingMode
Formatter.number.numberStyle = .decimal
return Formatter.number.string(for: self) ?? ""
}
}
extension String {
var doubleValue: Double? {
return Formatter.number.number(from: self)?.doubleValue
}
var intValue: Int? {
return Formatter.number.number(from: self)?.intValue
}
var floatValue: Float? {
return Formatter.number.number(from: self)?.floatValue
}
}
let num = 1.4988
num.fractionDigits() //1.49
num.fractionDigits(min: 2, max: 3, roundingMode: .down) //1.498
num.fractionDigits(min: 2, max: 3, roundingMode: .up) //1.499
let str = "123.88"
str.doubleValue //123.88
str.intValue //123
str.floatValue //123.88
str.floatValue?.fractionDigits(min: 1, max: 1, roundingMode: .up) //123.9
const t = const Tuple2<String, int>('a', 10);
print(t.item1); // prints 'a'
print(t.item2); // prints '10'
///Either
Either<SomeException, int> testEither(int code) {
if (code >= 100) return Right(200);
return Left(SomeException("不能小于 100"));
}
final result = testEither(100);
print(result.isRight); //true
print(result.isLeft); //false;
result.fold((lhs) => print(lhs), (rhs) => print(rhs));
import Alamofire
protocol APIConfiguration: URLRequestConvertible {
var method: HTTPMethod { get }
var path: String { get }
var parameters: Parameters? { get }
}
enum Router: APIConfiguration {
case topics
case search(keywords: String, page: Int)
case resource(id: String)
var method: HTTPMethod {
return .get
}
var path: String {
switch self {
case .topics:
return "/topics"
case .search:
return "/search"
case .resource(let id):
return "/resource/\(id)"
}
}
var parameters: Parameters? {
switch self {
case .search(let keywords, let page):
return ["wd": keywords, "p": page]
default:
return nil
}
}
var encoding: ParameterEncoding {
return URLEncoding.default
}
func asURLRequest() throws -> URLRequest {
let url = try ENV.host.asURL()
var request = URLRequest(url: url.appendingPathComponent(path))
request.httpMethod = method.rawValue
return try encoding.encode(request, with: parameters)
}
}
//使用
Alamofire.request(Router.topics)...
参考资料
Write a Networking Layer in Swift 4 using Alamofire 5 and Codable Part 1: API Router
void main() {
final result =
formatText("123你好 hello world 😞😞😍🤩🥳😇4😰🤯5🥱6🤐🤑🤧🤒🎃🦾😰😒😜");
print(result);
}
String formatText(String str) {
final RegExp regExp = RegExp(
r'(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])');
if (str.contains(regExp)) {
str = str.replaceAll(regExp, '');
}
return str;
}
//map
let strings = (1...5).map(String.init) //same as .map { String($0) }
print(strings) //["1", "2", "3", "4", "5"]
//filter
let names = ["Popeye", "Oyl", "Edison"].filter { $0.count > 3 }
print(names) //["Popeye", "Edison"]
//compactMap
let numbers = ["1", "2", "hello"].compactMap { Int($0) }
print(numbers) //[1, 2]
//flatMap
let flat = [[1,2,3], [4,5,6]].flatMap { $0 }
print(flat) //[1, 2, 3, 4, 5, 6]
//sorted
let sorted = flat.sorted(by: >)
print(sorted) //[6, 5, 4, 3, 2, 1]
//reduce
let sayHello = ["h", "e", "l", "l", "o"].reduce("Say ", +)
print(sayHello) // Say hello
let sum = [1,2,3,4,5].reduce(0, +)
print(sum) //15
//allSatisfy
print("allSatisfy: ", [1,2,3,4,5].allSatisfy { $0 > 3}) //allSatisfy: false
GOOS=linux GOARCH=amd64 go build -ldflags '-w -s' -o dogetv-cli dogetv-cli.go && upx ./dogetv-cli
GOOS=windows GOARCH=amd64 go build -ldflags '-w -s' -o dogetv-cli.exe dogetv-cli.go && upx ./dogetv-cli.exe
GOOS=darwin GOARCH=amd64 go build -ldflags '-w -s' -o dogetv-cli dogetv-cli.go && upx ./dogetv-cli
UPX: the Ultimate Packer for eXecutables
使用 Homebrew 安装
brew install upx
import UIKit
class CategoryViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var collectionView: UICollectionView!
var collectionViewIsScrollDown: Bool = false
var collectionViewLastOffsetY: CGFloat = 0
var categories: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView()
tableView.separatorColor = .white
tableView.backgroundColor = .groupTableViewBackground
tableView.showsVerticalScrollIndicator = false
tableView.scrollsToTop = false
tableView.register(CategoryTableViewCell.self, forCellReuseIdentifier: "cell")
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "cell")
collectionView.register(CollectionViewSectionHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header")
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
let width = (UIScreen.main.bounds.width - tableView.frame.width - 16) / 3
let height = width + 10
layout.itemSize = CGSize(width: width, height: height)
layout.sectionInset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
layout.minimumLineSpacing = 2
layout.minimumInteritemSpacing = 2
layout.sectionHeadersPinToVisibleBounds = true
layout.headerReferenceSize = CGSize(width: collectionView.frame.width, height: 44)
}
categories = (1...20).map{ "分类\($0)" }
tableView.selectRow(at: IndexPath(row: 0, section: 0), animated: true, scrollPosition: .none)
}
}
extension CategoryViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = categories[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
collectionViewScrollToTop(at: indexPath.row)
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
extension CategoryViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return categories.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 15
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header", for: indexPath) as! CollectionViewSectionHeader
header.titleLabel.text = categories[indexPath.section]
return header
}
func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {
if !collectionViewIsScrollDown && (collectionView.isDragging || collectionView.isDecelerating) {
tableView.selectRow(at: IndexPath(row: indexPath.section, section: 0), animated: true, scrollPosition: .top)
}
}
func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) {
if collectionViewIsScrollDown && (collectionView.isDragging || collectionView.isDecelerating) {
tableView.selectRow(at: IndexPath(row: indexPath.section + 1, section: 0), animated: true, scrollPosition: .top)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView == collectionView else { return }
let offsetY = scrollView.contentOffset.y
collectionViewIsScrollDown = collectionViewLastOffsetY < offsetY
collectionViewLastOffsetY = offsetY
}
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
guard scrollView == collectionView, !categories.isEmpty else { return }
tableView.selectRow(at: IndexPath(row: 0, section: 0), animated: false, scrollPosition: .none)
}
}
extension CategoryViewController {
func collectionViewScrollToTop(at section: Int, animated: Bool = true) {
let headerRect = collectionViewHeaderFrame(at: section)
let top = CGPoint(x: 0, y: headerRect.origin.y - collectionView.contentInset.top)
collectionView.setContentOffset(top, animated: animated)
}
func collectionViewHeaderFrame(at section: Int) -> CGRect {
let indexPath = IndexPath(item: 0, section: section)
let attributes = collectionView.collectionViewLayout.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: indexPath)
guard let firstCellFrame = attributes?.frame else { return .zero }
return firstCellFrame
}
}
class CategoryTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
textLabel?.font = .systemFont(ofSize: 12)
textLabel?.textAlignment = .center
backgroundColor = .groupTableViewBackground
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
contentView.backgroundColor = selected ? .white : .groupTableViewBackground
isHighlighted = selected
}
}
class CollectionViewSectionHeader: UICollectionReusableView {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
backgroundColor = .groupTableViewBackground
addSubview(titleLabel)
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
titleLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 8).isActive = true
titleLabel.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -8).isActive = true
}
}
class CollectionViewCell: UICollectionViewCell {
lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "demo")
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "#Hello#"
label.textAlignment = .center
label.font = .systemFont(ofSize: 12)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
addSubview(imageView)
addSubview(titleLabel)
imageView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
titleLabel.topAnchor.constraint(greaterThanOrEqualTo: imageView.bottomAnchor, constant: 8).isActive = true
titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8).isActive = true
}
}
import UIKit
import QuartzCore
// 波浪曲线动画视图
// 波浪曲线公式:y = h * sin(a * x + b); h: 波浪高度, a: 波浪宽度系数, b: 波浪的移动
class WaveView: UIView {
// 波浪高度h
var waveHeight: CGFloat = 7
// 波浪宽度系数a
var waveRate: CGFloat = 0.01
// 波浪移动速度
var waveSpeed: CGFloat = 0.05
// 真实波浪颜色
var realWaveColor: UIColor = .blue
// 阴影波浪颜色
var maskWaveColor: UIColor = UIColor.blue.withAlphaComponent(0.5)
// 波浪位置(默认是在下方)
var waveOnBottom = true
// 定时器
private var displayLink: CADisplayLink?
// 真实波浪
private var realWaveLayer: CAShapeLayer?
// 阴影波浪
private var maskWaveLayer: CAShapeLayer?
// 波浪的偏移量
private var offset: CGFloat = 0
// 视图初始化
override init(frame: CGRect) {
super.init(frame: frame)
initWaveParameters()
}
// 视图初始化
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initWaveParameters()
}
// 组件初始化
private func initWaveParameters() {
// 真实波浪配置
realWaveLayer = CAShapeLayer()
var frame = bounds
realWaveLayer?.frame.origin.y = frame.height - waveHeight
frame.size.height = waveHeight
realWaveLayer?.frame = frame
// 阴影波浪配置
maskWaveLayer = CAShapeLayer()
maskWaveLayer?.frame.origin.y = frame.height - waveHeight
frame.size.height = waveHeight
maskWaveLayer?.frame = frame
layer.addSublayer(maskWaveLayer!)
layer.addSublayer(realWaveLayer!)
}
// 开始播放动画
func startWave() {
// 开启定时器
displayLink = CADisplayLink(target: self, selector: #selector(wave))
displayLink?.add(to: .current, forMode: .common)
}
// 停止播放动画
func endWave() {
// 结束定时器
displayLink?.invalidate()
displayLink = nil
}
// 定时器响应(每一帧都会调用一次)
@objc func wave() {
// 波浪移动的关键:按照指定的速度偏移
offset += waveSpeed
// 起点y坐标(没有波浪的一侧)
let startY = waveOnBottom ? 0 : frame.height
let realPath = UIBezierPath()
realPath.move(to: CGPoint(x: 0, y: startY))
let maskPath = UIBezierPath()
maskPath.move(to: CGPoint(x: 0, y: startY))
var x = CGFloat(0)
var y = CGFloat(0)
while x <= bounds.width {
// 波浪曲线
y = waveHeight * sin(x * waveRate + offset)
// 如果是下波浪还要加上视图高度
let realY = y + (self.waveOnBottom ? frame.height : 0)
let maskY = -y + (self.waveOnBottom ? frame.height : 0)
realPath.addLine(to: CGPoint(x: x, y: realY))
maskPath.addLine(to: CGPoint(x: x, y: maskY))
// 增量越小,曲线越平滑
x += 0.1
}
// 回到起点对侧
realPath.addLine(to: CGPoint(x: frame.width, y: startY))
maskPath.addLine(to: CGPoint(x: frame.width, y: startY))
// 闭合曲线
maskPath.close()
// 把封闭图形的路径赋值给CAShapeLayer
maskWaveLayer?.path = maskPath.cgPath
maskWaveLayer?.fillColor = maskWaveColor.cgColor
realPath.close()
realWaveLayer?.path = realPath.cgPath
realWaveLayer?.fillColor = realWaveColor.cgColor
}
}
import Foundation
import CommonCrypto
protocol Compatible { }
struct Wrapper<Base> {
let base: Base
init(_ base: Base) {
self.base = base
}
}
extension Compatible {
var pp: Wrapper<Self> {
get { return Wrapper(self) }
set { }
}
}
extension String: Compatible {}
extension Wrapper where Base == String {
var md5: String {
guard let data = base.data(using: .utf8) else {
return base
}
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
_ = data.withUnsafeBytes { bytes in
return CC_MD5(bytes, CC_LONG(data.count), &digest)
}
return digest.map { String(format: "%02x", $0) }.joined()
}
}
// 调用时,使用 xxx.pp.xxx
"Hello".pp.md5 // "8b1a9953c4611296a827abf8c47804d7"
import 'package:flutter/foundation.dart';
import 'dart:async';
class Debouncer {
final int milliseconds;
VoidCallback action;
Timer _timer;
Debouncer({ this.milliseconds });
run(VoidCallback action) {
if (_timer != null) {
_timer.cancel();
}
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
}
trigger
final _debouncer = Debouncer(milliseconds: 500);
onTextChange(String text) {
_debouncer.run(() => print(text));
}
import PackageDescription
let package = Package(
name: "dogetv-cli",
dependencies: [
.package(url: "https://github.com/jakeheis/SwiftCLI", from: "5.2.2"),
.package(url: "https://github.com/onevcat/Rainbow", from: "3.0.0")
],
targets: [
.target(
name: "dogetv-cli",
dependencies: ["SwiftCLI", "Rainbow"]),
]
)
#if os(OSX)
import Darwin
#else
import Glibc
#endif
import Foundation
import SwiftCLI
import Rainbow
let HOST = "https://tv.popeye.vip"
func makeRequest<T: Decodable> (url: URL, type: T.Type) -> T? {
let sema: DispatchSemaphore = DispatchSemaphore(value: 0)
var result: T? = nil
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
result = try? JSONDecoder().decode(T.self, from: data)
}
sema.signal()
}
task.resume()
sema.wait()
return result
}
func getBody(from url: URL) -> String? {
let sema: DispatchSemaphore = DispatchSemaphore(value: 0)
var result: String? = nil
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
result = String.init(data: data, encoding: .utf8)
}
sema.signal()
}
task.resume()
sema.wait()
return result
}
class SearchCommand: Command {
let name: String = "search"
let shortDescription: String = "搜索电影/电视剧/综艺/影人"
let keyword = Parameter()
func execute() throws {
let url = URL(string: "\(HOST)/pumpkin/search/\(keyword.value.encodedURI)")!;
let result = makeRequest(url: url, type: Response<[Video]>.self)
guard let videos = result?.data, !videos.isEmpty else {
stderr <<< "未找到相关资源".yellow
return
}
let output = videos.enumerated().reduce("") { (result, item) -> String in
return result + "\n[\(String(item.offset).blue.underline)] \(item.element.name)"
}
stdout <<< output
let msg = "区间 0 ~ \(videos.count - 1)".red
let index = Input.readInt(prompt: "Input index:".lightCyan, validation: [.greaterThan(-1, message: msg), .lessThan(videos.count, message: msg)])
let video = videos[index]
if !video.cover.contains("vcinema") {
_ = cli.go(with: ["blueray-episodes", video.id])
return
}
_ = cli.go(with: ["detail", video.id])
}
}
class VideoDetailCommand: Command {
let name: String = "detail"
let shortDescription: String = "视频信息"
let id = Parameter()
func execute() throws {
let url = URL(string: "\(HOST)/pumpkin/video/\(id.value)")!
let result = makeRequest(url: url, type: Response<VideoDetail>.self)
guard let videoDetail = result?.data else {
stderr <<< "未找到相关资源".yellow
return
}
guard let seasons = videoDetail.seasons, !seasons.isEmpty else{
_ = cli.go(with: ["episodes", id.value])
return
}
let output = seasons.enumerated().reduce("") { (result, item) -> String in
return result + (item.offset % 4 == 0 ? "\n" : "") + "[\(String(item.offset).blue.underline)] \(item.element.name)\t"
}
stdout <<< output
let msg = "有效区间 0 ~ \(seasons.count - 1)".red
let index = Input.readInt(prompt: "Input index:".lightCyan, validation: [.greaterThan(-1, message: msg), .lessThan(seasons.count, message: msg)])
let seasonId = seasons[index].id
_ = cli.go(with: ["season-episodes", id.value, seasonId])
}
}
class VideoEpisodesCommand: Command {
let name: String = "episodes"
let shortDescription: String = "播放列表"
let id = Parameter()
func execute() throws {
let url = URL(string: "\(HOST)/pumpkin/video/\(id.value)/stream")!
let result = makeRequest(url: url, type: Response<[Episode]>.self)
guard let episodes = result?.data, !episodes.isEmpty else {
stderr <<< "未找到相关资源".yellow
return
}
let output = episodes.enumerated().reduce("") { (result, item) -> String in
return result + (item.offset % 4 == 0 ? "\n" : "") + "[\(String(item.offset).blue.underline)] \(item.element.title)\t"
}
stdout <<< output
let msg = "有效区间 0 ~ \(episodes.count - 1)".red
let index = Input.readInt(prompt: "Input index:".lightCyan, validation: [.greaterThan(-1, message: msg), .lessThan(episodes.count, message: msg)])
let streamUrl = episodes[index].url
do {
try run("open", "iina://weblink?url=\(streamUrl.encodedURI)")
} catch {
stderr <<< error.localizedDescription.red
}
}
}
class SeasonEpisodesCommand: Command {
let name: String = "season-episodes"
let shortDescription: String = "分季播放列表"
let id = Parameter()
let seasonId = Parameter()
func execute() throws {
let url = URL(string: "\(HOST)/pumpkin/video/\(id.value)?sid=\(seasonId.value)")!
let result = makeRequest(url: url, type: Response<VideoDetail>.self)
guard let videoDetail = result?.data, let seasons = videoDetail.seasons, !seasons.isEmpty else {
stderr <<< "未找到相关资源".yellow
return
}
let season = seasons.first { $0.episodes?.isEmpty == false }
guard let episodes = season?.episodes, !episodes.isEmpty else {
stderr <<< "未找到相关资源".yellow
return
}
let output = episodes.enumerated().reduce("") { (result, item) -> String in
return result + (item.offset % 4 == 0 ? "\n" : "") + "[\(String(item.offset).blue.underline)] \(item.element.title)\t"
}
stdout <<< output
let msg = "有效区间 0 ~ \(episodes.count - 1)".red
let index = Input.readInt(prompt: "Input index:".lightCyan.underline, validation: [.greaterThan(-1, message: msg), .lessThan(episodes.count, message: msg)])
guard let episodeId = episodes[index].id else {
stderr <<< "未找到相关资源".yellow
return
}
_ = cli.go(with: ["episodes", episodeId])
}
}
class BluerayEpisodesCommand: Command {
let name: String = "blueray-episodes"
let shortDescription: String = "高清资源播放列表"
let id = Parameter()
func execute() throws {
let url = URL(string: "\(HOST)/4k/detail/\(id.value)/episodes")!
let result = makeRequest(url: url, type: Response<[Episode]>.self)
guard let episodes = result?.data, !episodes.isEmpty else {
stderr <<< "未找到相关资源".yellow
return
}
let output = episodes.enumerated().reduce("") { (result, item) -> String in
return result + (item.offset % 4 == 0 ? "\n" : "") + "[\(String(item.offset).blue.underline)] \(item.element.title)\t"
}
stdout <<< output
let msg = "有效区间 0 ~ \(episodes.count - 1)".red
let index = Input.readInt(prompt: "Input index:".lightCyan, validation: [.greaterThan(-1, message: msg), .lessThan(episodes.count, message: msg)])
guard let episodeUrl = URL(string: episodes[index].url) else {
stderr <<< "未找到相关资源".yellow
return
}
guard let body = getBody(from: episodeUrl) else { return }
guard let regex = try? NSRegularExpression(pattern: "(http:|https:)//(.*).m3u8") else { return }
guard let matched = regex.firstMatch(in: body, range: NSRange(body.startIndex..., in: body)) else {
return
}
let m3u8 = String(body[Range(matched.range, in: body)!])
do {
try run("open", "iina://weblink?url=\(m3u8.encodedURI)")
} catch {
stderr <<< error.localizedDescription.yellow
}
}
}
let cli = CLI(name: "dogetv-cli", version: "1.0.0", description: "搜索电影/电视剧/综艺/影人", commands: [SearchCommand(), VideoDetailCommand(), VideoEpisodesCommand(), SeasonEpisodesCommand(), BluerayEpisodesCommand()])
_ = cli.go()
#if os(OSX)
import Darwin
#else
import Glibc
#endif
import Foundation
extension String {
public var encodedURI: String {
var characterSet = CharacterSet.alphanumerics
characterSet.insert(charactersIn: "-_.!~*'()")
return self.addingPercentEncoding(withAllowedCharacters: characterSet) ?? self
}
}
#if os(OSX)
import Darwin
#else
import Glibc
#endif
import Foundation
public struct Response<T: Decodable>: Decodable {
public let code: Int
public let msg: String
public let data: T
}
public struct IPTV: Decodable {
public let id: String
public let category: String
}
public struct Video: Decodable {
public let id: String
public let name: String
public let cover: String
}
public struct Season: Decodable {
public let id: String
public let name: String
public let episodes: [Episode]?
}
public struct Episode: Decodable {
public let id: String?
public let url: String
public let title: String
}
public struct VideoDetail: Decodable {
public let info: Video
public let seasons: [Season]?
}
$swift build -c release
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/urfave/cli"
)
const (
API_HOST = "https://tv.popeye.vip"
)
type Video struct {
Name string `json:"name"`
ID string `json:"id"`
Area string `json:"area"`
Year string `json:"year"`
Cover string `json:"cover"`
}
func main() {
app := cli.NewApp()
app.Name = "dogetv-cli"
app.Usage = "搜索影视资源"
app.Version = "1.0.0"
app.Author = "Popeye Lau"
app.Email = "[email protected]"
app.UsageText = "dogetv-cli search -k 权力的游戏"
app.Commands = []cli.Command{
{
Name: "search",
Usage: "按关键字搜索电影/演员/导演",
UsageText: "dogetv-cli search -k 权力的游戏",
Flags: []cli.Flag{
cli.StringFlag{
Name: "keyword, k",
Usage: "关键字",
},
},
Action: searchAction,
Description: "搜索",
},
}
app.Run(os.Args)
}
func searchAction(c *cli.Context) error {
keyword := strings.TrimSpace(c.String("keyword"))
if len(keyword) == 0 {
keyword = readStringFromStdIn()
}
api := fmt.Sprintf("%v/pumpkin/search/%v", API_HOST, keyword)
var resp struct {
Data []Video `json:"data"`
}
if ok, err := readJSONFromURL(api, http.MethodGet, nil, &resp); !ok {
fmt.Fprintf(c.App.Writer, "\n\033[1;31m%s\033[0m", err.Error())
return nil
}
if len(resp.Data) == 0 {
fmt.Fprintf(c.App.Writer, "\n\033[1;31m%s\033[0m", "未找到匹配资源")
return nil
}
for index, item := range resp.Data {
fmt.Fprintf(c.App.Writer, "\n\033[1;36m[%d]\033[0m %s", index, item.Name)
}
index := readIntFromStdIn(len(resp.Data))
return fetchVideoInfo(resp.Data[index], c)
}
func fetchVideoInfo(video Video, c *cli.Context) error {
id := video.ID
api := fmt.Sprintf("%v/pumpkin/video/%v", API_HOST, id)
if !strings.Contains(video.Cover, "vcinema") {
return fetchEpisodes(fmt.Sprintf("%v/4k/detail/%v/episodes", API_HOST, id), c)
}
var resp struct {
Data struct {
Info Video `json:"info"`
Seasons []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"seasons"`
} `json:"data"`
}
if ok, err := readJSONFromURL(api, http.MethodGet, nil, &resp); !ok {
fmt.Fprintf(c.App.Writer, "\n\033[1;31m%s\033[0m", err.Error())
return err
}
if len(resp.Data.Seasons) == 0 {
return fetchEpisodes(fmt.Sprintf("%v/pumpkin/video/%v/stream", API_HOST, id), c)
}
fmt.Fprint(c.App.Writer, "\n")
for i, v := range resp.Data.Seasons {
fmt.Fprintf(c.App.Writer, "\033[1;36m[%d]\033[0m %s\t", i, v.Name)
}
index := readIntFromStdIn(len(resp.Data.Seasons))
sid := resp.Data.Seasons[index].ID
return fetchSeasonEpisode(id, sid, c)
}
func fetchEpisodes(api string, c *cli.Context) error {
var resp struct {
Data []struct {
URL string `json:"url"`
Title string `json:"title"`
} `json:"data"`
}
if ok, err := readJSONFromURL(api, http.MethodGet, nil, &resp); !ok {
fmt.Fprintf(c.App.Writer, "\n\033[1;31m%s\033[0m", err.Error())
return err
}
if len(resp.Data) == 1 {
openIINA(resp.Data[0].URL)
return nil
}
fmt.Fprint(c.App.Writer, "\n")
for i, v := range resp.Data {
fmt.Fprintf(c.App.Writer, "\033[1;36m[%d]\033[0m %s\t", i, v.Title)
}
index := readIntFromStdIn(len(resp.Data))
openIINA(resp.Data[index].URL)
return nil
}
func fetchSeasonEpisode(id, sid string, c *cli.Context) error {
api := fmt.Sprintf("%v/pumpkin/video/%v?sid=%v", API_HOST, id, sid)
var resp struct {
Data struct {
Seasons []struct {
ID string `json:"id"`
Name string `json:"name"`
Episodes []struct {
ID string `json:"id"`
Title string `json:"title"`
} `json:"episodes"`
} `json:"seasons"`
} `json:"data"`
}
if ok, err := readJSONFromURL(api, http.MethodGet, nil, &resp); !ok {
fmt.Fprintf(c.App.Writer, "\n\033[1;31m%s\033[0m", err.Error())
return err
}
var seasonIndex int
for i, item := range resp.Data.Seasons {
if len(item.Episodes) > 0 {
seasonIndex = i
fmt.Fprint(c.App.Writer, "\n")
for index, episode := range item.Episodes {
fmt.Fprintf(c.App.Writer, "\033[1;36m[%d]\033[0m %s\t", index, episode.Title)
}
break
}
}
episodes := resp.Data.Seasons[seasonIndex].Episodes
index := readIntFromStdIn(len(episodes))
return fetchEpisodes(fmt.Sprintf("%v/pumpkin/video/%v/stream", API_HOST, episodes[index].ID), c)
}
func openIINA(episodeURL string) {
episodeURL, err := getStreamURL(episodeURL)
if err != nil {
fmt.Printf("\n\033[1;31m%s\033[0m", err.Error())
return
}
fmt.Println(episodeURL)
encodeURL := url.QueryEscape(episodeURL)
iina := fmt.Sprintf("iina://weblink?url=%s", encodeURL)
openBrowser(iina)
}
func getStreamURL(source string) (string, error) {
ext := filepath.Ext(source)
if !strings.HasPrefix(ext, ".m3u8") && !strings.HasPrefix(ext, ".mp4") {
url, err := fetchStreamURL(source)
if err != nil {
return "", err
}
return url, nil
}
return source, nil
}
func readJSONFromURL(url string, method string, params url.Values, target interface{}) (bool, error) {
client := &http.Client{}
req, err := http.NewRequest(method, url, strings.NewReader(params.Encode()))
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
body = bytes.Trim(body, " ();")
if err != nil {
return false, err
}
err = json.Unmarshal(body, target)
if err != nil {
return false, err
}
return true, nil
}
func fetchStreamURL(source string) (string, error) {
url, err := url.Parse(source)
if err != nil {
return "", err
}
client := &http.Client{}
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
html := string(body)
rp := regexp.MustCompile(`(http:|https:)\/\/(.*)\.m3u8`)
result := rp.FindString(html)
if len(result) == 0 {
return "", errors.New("获取失败")
}
return result, nil
}
func openBrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
func readIntFromStdIn(max int) int {
index := -1
fmt.Print("\n\033[1;33m序号> \033[0m")
for index < 0 || index >= max {
fmt.Scan(&index)
}
return index
}
func readStringFromStdIn() string {
var input string
fmt.Print("\n\033[1;33m关键字> \033[0m")
for len(strings.TrimSpace(input)) == 0 {
fmt.Scan(&input)
}
return input
}
A simple, fast, and fun package for building command line apps in Go
import 'dart:ui';
import 'package:flutter/material.dart';
extension TextStyleExtensions on TextStyle {
TextStyle clr(Color v) => copyWith(color: v);
TextStyle bgClr(Color v) => copyWith(backgroundColor: v);
TextStyle size(double v) => copyWith(fontSize: v);
TextStyle w(FontWeight v) => copyWith(fontWeight: v);
TextStyle get light => w(FontWeight.w200);
TextStyle get regular => w(FontWeight.normal);
TextStyle get semiBold => w(FontWeight.w500);
TextStyle get bold => w(FontWeight.bold);
TextStyle fs(FontStyle v) => copyWith(fontStyle: v);
TextStyle get italic => fs(FontStyle.italic);
TextStyle get normal => fs(FontStyle.normal);
TextStyle ltrSpace(double v) => copyWith(letterSpacing: v);
TextStyle wrdSpace(double v) => copyWith(wordSpacing: v);
TextStyle bsl(TextBaseline v) => copyWith(textBaseline: v);
TextStyle lnHeight(double v) => copyWith(height: v);
TextStyle loc(Locale v) => copyWith(locale: v);
TextStyle fg(Paint v) => copyWith(foreground: v);
TextStyle bg(Paint v) => copyWith(background: v);
TextStyle shdwList(List<Shadow> v) => copyWith(shadows: v);
TextStyle ftList(List<FontFeature> v) => copyWith(fontFeatures: v);
TextStyle dec(TextDecoration v, {Color color, TextDecorationStyle style, double thickness}) =>
copyWith(decoration: v, decorationColor: color, decorationStyle: style, decorationThickness: thickness);
TextStyle decClr(Color v) => copyWith(decorationColor: v);
TextStyle decStyle(TextDecorationStyle v) => copyWith(decorationStyle: v);
TextStyle decThick(double v) => copyWith(decorationThickness: v);
}
import UIKit
func heightOfString(withConstrainedWidth width: CGFloat,
attributes: [NSAttributedString.Key: Any],
insets: UIEdgeInsets = .zero) -> CGFloat {
let constraintRect = CGSize(width: width - insets.left - insets.right, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect,
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: attributes, context: nil)
return ceil(boundingBox.height) + insets.top + insets.bottom
}
var trimed: String {
return trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
}
//使用
let str = "注意该函数和 ReadAtLeast 的区别:ReadFull 将 buf 读满;而 ReadAtLeast 是最少读取 min 个字节。"
let insets = UIEdgeInsets(top: 20, left: 8, bottom: 20, right: 8)
//行间距
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 5
//样式
let attributes:[NSAttributedString.Key: Any] = [.font: UIFont.preferredFont(forTextStyle: .caption2), .paragraphStyle: paragraphStyle]
//计算高度
let textHeight = data.desc.trimed.heightOfString(withConstrainedWidth: bounds.width, attributes: attributes, insets: insets)
Style | Font | Size |
---|---|---|
.largeTitle | SFUIDisplay | 34.0 |
.title1 | SFUIDisplay (-Light on iOS <=10) |
28.0 |
.title2 | SFUIDisplay | 22.0 |
.title3 | SFUIDisplay | 20.0 |
.headline | SFUIText-Semibold | 17.0 |
.callout | SFUIText | 16.0 |
.subheadline | SFUIText | 15.0 |
.body | SFUIText | 17.0 |
.footnote | SFUIText | 13.0 |
.caption1 | SFUIText | 12.0 |
.caption2 | SFUIText | 11.0 |
//
// SelectionViewController.swift
// dogeTV
//
// Created by Popeye Lau on 2019/3/28.
// Copyright © 2019 Popeye Lau. All rights reserved.
//
import UIKit
class PopoverPresentation: NSObject, UIPopoverPresentationControllerDelegate {
static let sharedInstance = PopoverPresentation()
override init() {
super.init()
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
static func configurePresentation(forController controller : UIViewController) -> UIPopoverPresentationController {
controller.modalPresentationStyle = .popover
let presentationController = controller.presentationController as! UIPopoverPresentationController
presentationController.backgroundColor = .white
presentationController.delegate = PopoverPresentation.sharedInstance
return presentationController
}
}
class SelectionViewController<Element> : UITableViewController {
typealias SelectionHandler = (Element) -> Void
typealias LabelProvider = (Element) -> String
private let values : [Element]
private let labels : LabelProvider
private let onSelect : SelectionHandler?
init(_ values : [Element], labels : @escaping LabelProvider = String.init(describing:), onSelect : SelectionHandler? = nil) {
self.values = values
self.onSelect = onSelect
self.labels = labels
super.init(style: .plain)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.separatorColor = .groupTableViewBackground
tableView.showsVerticalScrollIndicator = false
tableView.separatorInset = .zero
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return values.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.textLabel?.text = labels(values[indexPath.row])
cell.textLabel?.textColor = .darkGray
cell.textLabel?.font = .systemFont(ofSize: 13)
cell.imageView?.image = UIImage(named: "line")
cell.imageView?.tintColor = .darkGray
cell.textLabel?.textAlignment = .center
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.dismiss(animated: true)
onSelect?(values[indexPath.row])
}
}
@objc func switchResource(_ sender: UIButton) {
guard let video = media, video.source > 0 else {
showInfo("没有其它路线可用")
return
}
if selectionController == nil {
let sources = Array(0..<min(video.source, 8))
selectionController = SelectionViewController(sources, labels: { "线路 \($0+1)" } ){ [weak self] (source) in
self?.resourceIndex = source
self?.refreshResource(with: source)
}
selectionController?.preferredContentSize = CGSize(width: 130, height: sources.count * 44)
}
showSourceSelectionView(with: sender)
}
func showSourceSelectionView(with sourceView: UIView) {
guard let controller = selectionController else {
return
}
let presentationController = PopoverPresentation.configurePresentation(forController: controller)
presentationController.sourceView = sourceView
presentationController.sourceRect = sourceView.bounds.insetBy(dx: -10, dy: 0)
presentationController.permittedArrowDirections = [.left]
present(controller, animated: true)
}
name
:包名version
:当前版本(注意,是当前版本,假如你后续更新了新版本,需要修改此处)summary
:简要描述,在pod search ZCPKit的时候会显示该信息。description
:详细描述homepage
:页面链接license
:开源协议author
:作者source
:源码git地址platform
:平台 & 版本号source_files
:源文件(可以包含.h和.m)public_header_files
:头文件(.h文件)resources
:资源文件(配置的文件都会被放到mainBundle中)resource_bundles
:资源文件(配置的文件会放到你自己指定的bundle中)frameworks
:依赖的系统框架vendored_frameworks
:依赖的非系统框架libraries
:依赖的系统库,需要去除前缀lib,如静态库依赖是libz.tbd,则s.libraries = 'z'vendored_libraries
:依赖的非系统的静态库dependency
:依赖的三方库import 'package:flutter/material.dart';
class ContextUtils {
// Takes a key, and in 1 frame, returns the size of the context attached to the key
static void getFutureSizeFromGlobalKey(GlobalKey key, Function(Size size) callback) {
Future.microtask(() {
Size size = getSizeFromContext(key.currentContext);
if (size != null) {
callback(size);
}
});
}
// Shortcut to get the renderBox size from a context
static Size getSizeFromContext(BuildContext context) {
RenderBox rb = context.findRenderObject() as RenderBox;
return rb?.size ?? Size.zero;
}
// Shortcut to get the global position of a context
static Offset getOffsetFromContext(BuildContext context, [Offset offset = null]) {
RenderBox rb = context.findRenderObject() as RenderBox;
return rb?.localToGlobal(offset ?? Offset.zero);
}
}
Splash.imageset
Splash.imageset/Contents.json
内容如下{
"images": [
{
"idiom": "iphone",
"scale": "1x"
},
{
"idiom": "iphone",
"filename": "640*960.png",
"scale": "2x"
},
{
"idiom": "iphone",
"filename": "1125*2436.png",
"scale": "3x"
},
{
"idiom": "iphone",
"subtype": "retina4",
"scale": "1x"
},
{
"idiom": "iphone",
"filename": "640*1136.png",
"subtype": "retina4",
"scale": "2x"
},
{
"idiom": "iphone",
"subtype": "retina4",
"scale": "3x"
},
{
"idiom": "iphone",
"filename": "1242*2208.png",
"subtype": "736h",
"scale": "3x"
},
{
"idiom": "iphone",
"filename": "750*1334.png",
"subtype": "667h",
"scale": "2x"
},
{
"idiom": "iphone",
"filename": "1125*2436.png",
"subtype": "2436h",
"scale": "3x"
},
{
"idiom": "iphone",
"filename": "1242*2688.png",
"subtype": "2688h",
"scale": "3x"
},
{
"idiom": "iphone",
"filename": "828*1792.png",
"subtype": "1792h",
"scale": "2x"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
LaunchScreen.storyboard
添加 UIImageView
设好约束,图片选择 Splash
即可Flutter 中读取 iOS Assets.xcassets 图片资源
iOS 读取 Flutter assets 图片资源
安装官方插件 ios_platform_images
import 'package:flutter/material.dart';
class PopupMenuPage extends StatefulWidget {
@override
_PopupMenuPageState createState() => _PopupMenuPageState();
}
class _PopupMenuPageState extends State<PopupMenuPage> {
static const List<String> menus = ["hello", "world"];
bool isOpen;
String selected;
final GlobalKey<PopupMenuButtonState<String>> key = GlobalKey();
@override
void initState() {
super.initState();
isOpen = false;
selected = menus.last;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black87,
body: Center(
child: PopupMenuButton<String>(
key: key,
padding: EdgeInsets.zero,
itemBuilder: (context) => menus.map((e) {
final color = selected == e ? Colors.orange : Colors.black;
return PopupMenuItem(
value: e,
child: Container(
width: 120,
child: Row(
children: [
Icon(Icons.keyboard_arrow_right, color: color),
SizedBox(width: 6),
Text(e, style: TextStyle(color: color))
],
),
),
);
}).toList(),
child: GestureDetector(
onTap: () {
key.currentState.showButtonMenu();
setState(() {
isOpen = !isOpen;
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: isOpen
? BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8))
: BorderRadius.circular(8),
color: Colors.white),
alignment: Alignment.center,
width: 100,
height: 40,
child: Text("Open", style: TextStyle(color: Colors.black)),
),
),
offset: Offset(20, 39),
elevation: 0,
onSelected: (e) {
closePopup(value: e);
},
onCanceled: closePopup,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8))),
),
),
);
}
closePopup({String value}) {
Future.delayed(Duration(milliseconds: 300), () {
setState(() {
isOpen = false;
selected = value ?? selected;
});
});
}
}
class Device {
// 以 iPhone 6 尺寸为设计稿
static let base: CGFloat = 375
static var ratio: CGFloat {
return UIScreen.main.bounds.width / base
}
}
xtension CGFloat {
var adjusted: CGFloat {
return self * Device.ratio
}
}
extension Double {
var adjusted: CGFloat {
return CGFloat(self) * Device.ratio
}
}
extension Int {
var adjusted: CGFloat {
return CGFloat(self) * Device.ratio
}
}
label.font = UIFont.systemFont(ofSize: 23.adjusted)
phoneTextField.leftAnchor.constraint(equalTo: container.leftAnchor, constant: 30.adjusted)
phoneTextField.rightAnchor.constraint(equalTo: container.rightAnchor, constant: -30.adjusted)
imageView.widthAnchor.constraint(equalToConstant: 80.adjusted)
imageView.heightAnchor.constraint(equalToConstant: 90.adjusted)
iOS 禁用 H5页面视频全屏
flutter/plugins#3334
flutter/flutter#25630
使用 Safari 替换脚本文件
找到对应的脚本文件,右键
- 创建本地覆盖
Android 允许混合模式
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
webView.getSettings().setBlockNetworkImage(false);
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
import UIKit
class SectionHeader: UITableViewHeaderFooterView {
lazy var shapeLayer: CAShapeLayer = CAShapeLayer()
let cornerRadius: CGFloat = 10
let inset: CGFloat = 10
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
setup()
}
func setup() {
backgroundView = UIView(frame: bounds)
backgroundView?.backgroundColor = .black
layer.mask = shapeLayer
}
override var frame: CGRect {
get { return super.frame }
set {
var frame = newValue
frame.origin.x += inset
frame.origin.y += inset
frame.size.width -= inset * 2
frame.size.height -= inset
super.frame = frame
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let bezierPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
shapeLayer.path = bezierPath.cgPath
}
}
class TableViewCell: UITableViewCell {
let inset: CGFloat = 10
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .lightGray
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var frame: CGRect {
get { return super.frame }
set {
var frame = newValue
frame.origin.x += inset
frame.size.width -= inset * 2
super.frame = frame
}
}
}
class ViewController: UIViewController {
lazy var tableView: UITableView = {
let tableView = UITableView(frame: view.bounds, style: .grouped)
tableView.register(TableViewCell.self, forCellReuseIdentifier: "cell")
tableView.register(SectionHeader.self, forHeaderFooterViewReuseIdentifier: "header")
tableView.tableFooterView = UIView()
tableView.dataSource = self
tableView.delegate = self
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "\(indexPath.section) - \(indexPath.row)"
cell.accessoryType = .disclosureIndicator
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let numberOfRowInSection = self.tableView(tableView, numberOfRowsInSection: indexPath.section)
guard indexPath.row == numberOfRowInSection - 1 else {
cell.layer.mask = nil
return
}
let shapeLayer = CAShapeLayer()
var bounds = cell.bounds
bounds.size.height -= 1
let bezierPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 10, height: 10))
shapeLayer.path = bezierPath.cgPath
cell.layer.mask = shapeLayer
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header") else {
return nil
}
return header
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 60
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 40
}
}
下载解压后复制到:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class LoginForm {
String phone;
String password;
LoginForm({this.phone = "", this.password = ""});
@override
String toString() => '{"phone": "$phone", "password": "$password"}';
}
@immutable
class FormPage extends StatefulWidget {
const FormPage({Key key}) : super(key: key);
@override
State<FormPage> createState() => _FormPageState();
}
class _FormPageState extends State<FormPage> {
///表单验证状态(控制登录按钮状态)
final ValueNotifier<bool> _allFilled = ValueNotifier(false);
///表单数据
LoginForm _loginForm;
///用户名
FieldStatus<String, LoginForm> _phoneField;
///密码
FieldStatus<String, LoginForm> _pwdField;
///表单项
Iterable<FieldStatus> _fields;
///按钮焦点
final FocusNode _buttonFocusNode = FocusNode();
@override
void initState() {
super.initState();
_loginForm = LoginForm();
_phoneField = FieldStatus<String, LoginForm>(
formData: _loginForm,
value: (data) => data.phone,
onChanged: (data, phone) => data.phone = phone,
validator: (data, phone) => phone?.isEmpty ?? true ? '手机号不能为空' : null,
);
_pwdField = FieldStatus<String, LoginForm>(
formData: _loginForm,
value: (data) => data.password,
onChanged: (data, pwd) => data.password = pwd,
validator: (data, pwd) => pwd?.isEmpty ?? true ? '密码不能为空' : null,
);
_fields = <FieldStatus>[_phoneField, _pwdField];
_setValidationStatus();
}
@override
void dispose() {
_allFilled.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Login")),
body: SafeArea(
child: Center(
child: Card(
margin: EdgeInsets.all(16),
elevation: 4,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
child: Padding(padding: EdgeInsets.all(18), child: form),
),
),
),
);
}
Widget get form => Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: _setValidationStatus,
child: Builder(
builder: (context) => SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
autofocus: true,
focusNode: _phoneField.focusNode,
onEditingComplete: _nextEmpty,
initialValue: _loginForm.phone,
onSaved: _phoneField.onChanged,
onChanged: _phoneField.onChanged,
validator: _phoneField.validator,
keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(labelText: '手机号'),
),
const SizedBox(height: 10),
TextFormField(
focusNode: _pwdField.focusNode,
onEditingComplete: _nextEmpty,
initialValue: _loginForm.password,
onSaved: _pwdField.onChanged,
onChanged: _pwdField.onChanged,
validator: _pwdField.validator,
keyboardType: TextInputType.text,
obscureText: true,
decoration: const InputDecoration(labelText: '密码'),
),
const SizedBox(height: 10),
ValueListenableBuilder<bool>(
valueListenable: _allFilled,
builder: (context, value, _) => RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
focusNode: _buttonFocusNode,
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).buttonColor,
onPressed: value ? () => _submit(context) : null,
child: const Text('登录')),
),
],
),
),
),
);
void _nextEmpty() {
final fieldNode = _fields
.firstWhere((field) => !field.filled, orElse: () => null)
?.focusNode;
if (fieldNode == null) {
_unfocusAll();
} else {
fieldNode.requestFocus();
}
}
void _unfocusAll() {
_fields
.map<FocusNode>((field) => field.focusNode)
.forEach((node) => node.unfocus());
}
void _setValidationStatus() {
_allFilled.value = _fields.every((field) => field.filled);
}
void _submit(BuildContext context) {
_unfocusAll();
final form = Form.of(context)..save();
if (!form.validate()) {
_nextEmpty();
return;
}
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('SUBMIT: $_loginForm'),
),
);
}
}
@immutable
class FieldStatus<FieldType, FormDataType> {
final FormDataType _formData;
bool get filled => !hasValidator || validator(value(_formData)) == null;
final FieldType Function(FormDataType data) value;
final FocusNode focusNode;
ValueChanged<FieldType> get onChanged =>
(value) => _onChanged(_formData, value);
final void Function(FormDataType data, FieldType value) _onChanged;
FormFieldValidator<FieldType> get validator =>
(value) => hasValidator ? _validator(_formData, value) : null;
final String Function(FormDataType data, FieldType value) _validator;
final bool hasValidator;
///表单项状态
///[formData] 表单数据
///[value] 表单项 (formData.xxx)
///[onChanged] 数据更新
///[validator] 校验方法
FieldStatus({
@required FormDataType formData,
@required this.value,
@required void Function(FormDataType data, FieldType value) onChanged,
String Function(FormDataType data, FieldType value) validator,
FocusNode focusNode,
}) : _formData = formData,
focusNode = focusNode ?? FocusNode(),
_onChanged = onChanged,
_validator = validator,
hasValidator = validator != null;
}
DartPad: https://dartpad.dev/b6409e10de32b280b8938aa75364fa7b
osascript -e 'tell application "iOS Simulator" to quit'
osascript -e 'tell application "Simulator" to quit'
xcrun simctl erase all
poster_painter.dart
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';
import 'dart:math' as math show max;
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cache;
import 'utils.dart';
class PosterPainter {
/// 商品海报
/// [coverUrl] 商品图
/// [miniCodeUrl] 小程序码
/// [title] 商品名称
/// [price] 价格
/// [mktPrice] 市场价
/// [saleDate] 预售日期
/// [store] 门店
static Future<ByteData> drawPoster(
{String coverUrl,
String miniCodeUrl,
String title,
double price,
double mktPrice,
String saleDate,
String store}) async {
final imageUrls = <String>[
"https://front-xps-cdn.xsyx.xyz/2020/01/10/1468273837.png", //background
coverUrl, //cover
miniCodeUrl, //miniCode
];
final downloadManager = cache.DefaultCacheManager();
bool hasError = false;
final files = await Future.wait<cache.FileInfo>(
imageUrls.map(downloadManager.downloadFile).toList())
.catchError((e, s) {
hasError = true;
debugPrint(e);
});
if (hasError || files.length != imageUrls.length) return null;
final background = await Utils.loadImage(files[0].file.readAsBytesSync());
final cover = await Utils.loadImage(files[1].file.readAsBytesSync());
final miniCode = await Utils.loadImage(files[2].file.readAsBytesSync());
final canvasSize =
Size(background.width.toDouble(), background.height.toDouble());
final coverSize = Size(cover.width.toDouble(), cover.height.toDouble());
final miniCodeSize =
Size(miniCode.width.toDouble(), miniCode.height.toDouble());
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
final painter = Paint()
..style = PaintingStyle.fill
..color = Colors.white;
canvas.drawImage(background, Offset.zero, painter);
double scaledSize = math.max(coverSize.width, coverSize.height) * 0.75;
double x = (canvasSize.width - scaledSize) * 0.5;
double y = 100.0;
canvas.drawImageRect(
cover,
Rect.fromLTRB(0, 0, coverSize.width, coverSize.height),
Rect.fromLTWH(x, y, scaledSize, scaledSize),
painter);
canvas.drawImageRect(
miniCode,
Rect.fromLTRB(0, 0, miniCodeSize.width, miniCodeSize.height),
Rect.fromLTWH(x, canvasSize.height - 160, 150, 150),
painter);
final style = TextStyle(
fontSize: 32, color: Colors.black, fontWeight: FontWeight.normal);
final textWidth = canvasSize.width - x * 2;
TextPainter(
textAlign: TextAlign.left,
text: TextSpan(children: [
TextSpan(
text: "$title\n",
style: style.copyWith(height: 2, fontWeight: FontWeight.w500)),
TextSpan(
text: "¥$price ",
style: style.copyWith(
fontSize: 40, color: Colors.red, fontWeight: FontWeight.w600)),
TextSpan(
text: "¥$mktPrice",
style: style.copyWith(
decoration: TextDecoration.lineThrough, color: Colors.grey)),
TextSpan(
text: " 预售时间: $saleDate\n",
style: style.copyWith(color: Colors.grey.shade400)),
TextSpan(
text: "提货门店: $store",
style: style.copyWith(height: 2, color: Colors.orange))
]),
textDirection: TextDirection.ltr,
)
..layout(maxWidth: textWidth, minWidth: textWidth)
..paint(canvas, Offset(x, y + scaledSize));
final picture = recorder.endRecording();
final img = await picture.toImage(
canvasSize.width.toInt(), canvasSize.height.toInt());
return img.toByteData(format: ImageByteFormat.png);
}
}
utils.dart
import 'dart:async';
import 'dart:ui';
class Utils {
static Future<Image> loadImage(List<int> img) async {
final Completer<Image> completer = Completer();
decodeImageFromList(img, (Image img) {
return completer.complete(img);
});
return completer.future;
}
}
home_page.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_draw_poster/poster_painter.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
ByteData posterByteData;
@override
void initState() {
super.initState();
drawing();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xffcccccc),
appBar: AppBar(
title: Text('Drawing'),
),
body: Container(
padding: EdgeInsets.all(20),
alignment: Alignment.center,
child: poster),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.insert_photo),
onPressed: drawing,
),
);
}
Widget get poster {
if (posterByteData == null) return null;
return Image.memory(Uint8List.view(posterByteData.buffer));
}
Future<void> drawing() async {
final bytes = await PosterPainter.drawPoster(
coverUrl: "https://front-xps-cdn.xsyx.xyz/2020/12/04/241721671.jpg",
miniCodeUrl: "https://front-xps-cdn.xsyx.xyz/2020/12/04/1348955141.jpg",
title: "精卫 精品红提 230g/盒 正负 20g",
price: 2.99,
mktPrice: 5.99,
saleDate: "12月12日",
store: "麓谷总部店");
setState(() {
posterByteData = bytes;
});
}
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.