├── .gitignore ├── LICENSE ├── README.md ├── lib ├── app │ ├── config │ │ └── config.dart │ ├── model │ │ ├── movie.dart │ │ ├── player.dart │ │ ├── push.dart │ │ └── site.dart │ ├── tool │ │ ├── decode.dart │ │ ├── http.dart │ │ ├── random.dart │ │ ├── search.dart │ │ └── time.dart │ └── widget │ │ ├── myBlurButton.dart │ │ ├── myBottomInput.dart │ │ ├── myBottomSheet.dart │ │ ├── myBottomTip.dart │ │ ├── myImage.dart │ │ ├── myRoundButton.dart │ │ ├── myText.dart │ │ ├── myTextField.dart │ │ ├── myTips.dart │ │ └── myToast.dart ├── generated_plugin_registrant.dart ├── main.dart └── pages │ ├── detail.dart │ ├── home.dart │ ├── play.dart │ ├── playx.dart │ ├── searchList.dart │ ├── share.dart │ ├── tv.dart │ └── video │ ├── after_layout.dart │ ├── controller_widget.dart │ ├── video_player_UI.dart │ ├── video_player_control.dart │ ├── video_player_pan.dart │ └── video_player_slider.dart ├── pubspec.yaml └── screenshots ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── 5.jpg ├── 6.jpg ├── 7.jpg ├── 8.jpg └── logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # Avoid committing generated Javascript files: 15 | *.dart.js 16 | *.info.json # Produced by the --dump-info flag. 17 | *.js # When generated by dart2js. Don't specify *.js if your 18 | # project includes source files written in JavaScript. 19 | *.js_ 20 | *.js.deps 21 | *.js.map 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 |

6 | 飞鱼 7 |

8 |

9 | 10 |

11 | 12 | # 简介 13 | 14 | Feiyu is a video player, which is developed using Flutter and supports #.m3u8, #.rtsp, #.rtmp video formats. 15 | 16 | You are free to add search sources within the application that complies with the FeiFeiCms interface specification or Live feeds in #.dpl, #.m3u, #.xspf formats. 17 | 18 | Meanwhile, you can also collect your favorite movies and make a movie list to record your movie-going history. That's all, have a good time. 19 | 20 | 飞鱼是使用Flutter开发的支援 #.m3u8, #.rtsp, #.rtmp 格式的视频播放器。 21 | 22 | 你可以在应用内自由添加符合 FeiFeiCms 接口规范的搜索源或者 #.dpl, #.m3u, #.xspf 格式的直播源。 23 | 24 | 同时,你也可以收藏自己中意的影片并制作成影单,记录自己的观影历史,不负好时光。 25 | 26 | # 预览 27 | 28 | **旧版** *(已开源)* 29 | 30 | | ![](screenshots/1.jpg) | ![](screenshots/2.jpg) | ![](screenshots/3.jpg) | ![](screenshots/4.jpg) | 31 | | :------------: | :------------: | :------------: | :------------: | 32 | 33 | **新版** 34 | 35 | | ![](screenshots/5.jpg) | ![](screenshots/6.jpg) | ![](screenshots/7.jpg) | ![](screenshots/8.jpg) | 36 | | :------------: | :------------: | :------------: | :------------: | 37 | 38 | # 免责声明 39 | 40 | 本项目仅供 Flutter 学习交流。 41 | 42 | # 鸣谢 43 | 44 | befovy / fijkplayer *MIT License* 45 | https://github.com/befovy/fijkplayer/blob/master/LICENSE 46 | 47 | succlz123 / DLNA-Dart *Apache License* 48 | https://github.com/succlz123/DLNA-Dart/blob/master/LICENSE 49 | 50 | 51 | IwantBEStrong / video_player_full_funciton 52 | https://github.com/IwantBEStrong/video_player_full_funciton 53 | -------------------------------------------------------------------------------- /lib/app/config/config.dart: -------------------------------------------------------------------------------- 1 | String APP = '20200621'; 2 | 3 | String SERVER = 'http://x.xxx.xxx/xxxx'; 4 | 5 | String PUSH = r''' 6 | { 7 | "flag": "关", 8 | "force": "是", 9 | "info": "抱歉,当前版本飞鱼已停用,请关注公众号“乂乂又又”,回复“飞鱼”,获取最新版本!", 10 | "about": "飞鱼是本人兴趣爱好之作,发布纯粹是开源分享、仅供学习交流。本软件不得用于商业用途或从事违反中国人民共和国相关法律所禁止的活动,本人对于用户擅自使用本软件从事违法活动不承担任何责任(包括但不限于刑事责任、行政责任、民事责任)。若您继续使用本软件则表明您已完全阅读理解并同意上述约定,否则请立即关闭并卸载本软件。 想要了解更多信息,请关注我的公众号:乂乂又又。" 11 | } 12 | '''; 13 | 14 | String PLAYER = r''' 15 | { 16 | "server": "https://www.dplayer.tv/dp/", 17 | "title": "title", 18 | "m3u8": "url", 19 | "public": "是" 20 | } 21 | '''; 22 | 23 | String SITES = r''' 24 | [{ 25 | "name": "OK", 26 | "server": "http://www.apiokzy.com/", 27 | "title": ".*?target=\"_blank\">(.*?)", 28 | "link": "(.*?)", 30 | "xml": "http://cj.okzy.tv/inc/api.php", 31 | "item": "", 32 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)", 33 | "cover": "(.*?)(.*?)", 41 | "xml": "http://cj.yongjiuzyw.com/inc/api.php", 42 | "item": "", 43 | "m3u8": "name=\"copy_sel\" value=\"(.*?\\.m3u8?)\" checked=\"\">", 44 | "cover": "" 45 | }, 46 | { 47 | "name": "最大", 48 | "server": "http://www.zuidazy5.com/", 49 | "title": ".*?target=\"_blank\">(.*?)<", 50 | "link": "(.*?)", 52 | "xml": "http://www.zdziyuan.com/inc/api_zuidam3u8.php", 53 | "item": "", 54 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)", 55 | "cover": ".*?target=\"_blank\">(.*?)", 61 | "link": "(.*?)", 63 | "xml": "http://api.kbzyapi.com/inc/api.php", 64 | "item": "", 65 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)", 66 | "cover": ".*?target=\"_blank\">(.*?)", 72 | "link": "(.*?)", 74 | "xml": "http://api.zuixinapi.com/inc/api.php", 75 | "item": "", 76 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)", 77 | "cover": ".*?target=\"_blank\">(.*?)", 83 | "link": "(.*?)", 85 | "xml": "http://cj.wlzy.tv/inc/api_mac_m3u8.php", 86 | "item": "", 87 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)", 88 | "cover": ".*?target=\"_blank\">(.*?)", 94 | "link": "(.*?)", 96 | "xml": "https://www.mhapi123.com/inc/api_all.php", 97 | "item": "", 98 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)", 99 | "cover": " playlist; 6 | 7 | Movie({this.title, this.desp, this.cover, this.playlist}); 8 | 9 | Movie.fromJson(Map json) { 10 | title = json['title']; 11 | desp = json['desp']; 12 | cover = json['cover']; 13 | if (json['playlist'] != null) { 14 | playlist = new List(); 15 | json['playlist'].forEach((v) { 16 | playlist.add(new Playlist.fromJson(v)); 17 | }); 18 | } 19 | } 20 | 21 | Map toJson() { 22 | final Map data = new Map(); 23 | data['title'] = this.title; 24 | data['desp'] = this.desp; 25 | data['cover'] = this.cover; 26 | if (this.playlist != null) { 27 | data['playlist'] = this.playlist.map((v) => v.toJson()).toList(); 28 | } 29 | return data; 30 | } 31 | } 32 | 33 | class Playlist { 34 | String name; 35 | String m3u8; 36 | 37 | Playlist({this.name, this.m3u8}); 38 | 39 | Playlist.fromJson(Map json) { 40 | name = json['name']; 41 | m3u8 = json['m3u8']; 42 | } 43 | 44 | Map toJson() { 45 | final Map data = new Map(); 46 | data['name'] = this.name; 47 | data['m3u8'] = this.m3u8; 48 | return data; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/app/model/player.dart: -------------------------------------------------------------------------------- 1 | class Player { 2 | String server; 3 | String title; 4 | String m3u8; 5 | String public; 6 | 7 | Player({this.server, this.title, this.m3u8, this.public}); 8 | 9 | Player.fromJson(Map json) { 10 | server = json['server']; 11 | title = json['title']; 12 | m3u8 = json['m3u8']; 13 | public = json['public']; 14 | } 15 | 16 | Map toJson() { 17 | final Map data = new Map(); 18 | data['server'] = this.server; 19 | data['title'] = this.title; 20 | data['m3u8'] = this.m3u8; 21 | data['public'] = this.public; 22 | return data; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/app/model/push.dart: -------------------------------------------------------------------------------- 1 | class Push { 2 | String flag; 3 | String force; 4 | String info; 5 | String about; 6 | 7 | Push({this.flag, this.info, this.about, this.force}); 8 | 9 | Push.fromJson(Map json) { 10 | flag = json['flag']; 11 | force = json['force']; 12 | info = json['info']; 13 | about = json['about']; 14 | } 15 | 16 | Map toJson() { 17 | final Map data = new Map(); 18 | data['flag'] = this.flag; 19 | data['force'] = this.force; 20 | data['info'] = this.info; 21 | data['about'] = this.about; 22 | return data; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/app/model/site.dart: -------------------------------------------------------------------------------- 1 | class Site { 2 | String name; 3 | String server; 4 | String title; 5 | String link; 6 | String desp; 7 | String item; 8 | String m3u8; 9 | String cover; 10 | String xml; 11 | 12 | Site( 13 | {this.name, 14 | this.server, 15 | this.title, 16 | this.link, 17 | this.desp, 18 | this.item, 19 | this.m3u8, 20 | this.cover, 21 | this.xml}); 22 | 23 | Site.fromJson(Map json) { 24 | name = json['name']; 25 | server = json['server']; 26 | title = json['title']; 27 | link = json['link']; 28 | desp = json['desp']; 29 | item = json['item']; 30 | m3u8 = json['m3u8']; 31 | cover = json['cover']; 32 | xml = json['xml']; 33 | } 34 | 35 | Map toJson() { 36 | final Map data = new Map(); 37 | data['name'] = this.name; 38 | data['server'] = this.server; 39 | data['title'] = this.title; 40 | data['link'] = this.link; 41 | data['desp'] = this.desp; 42 | data['item'] = this.item; 43 | data['m3u8'] = this.m3u8; 44 | data['cover'] = this.cover; 45 | data['xml'] = this.xml; 46 | return data; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/app/tool/decode.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | String encodex(String s) => urlEncode(b64(s)); 4 | 5 | String b64(s) => base64Encode(utf8.encode(s)); 6 | 7 | String urlEncode(String s) => Uri.encodeComponent(s); 8 | 9 | String toBase64(String data) { 10 | var content = utf8.encode(data); 11 | var digest = base64Encode(content); 12 | return digest; 13 | } 14 | 15 | String fromBase64(String data) { 16 | return utf8.decode(base64Decode(data)); 17 | } 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /lib/app/tool/http.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class Http { 4 | Dio dio; 5 | BaseOptions options; 6 | 7 | Http() { 8 | options = new BaseOptions( 9 | contentType: "application/x-www-form-urlencoded", 10 | responseType: ResponseType.plain, //以文本方式接收数据 11 | headers: { 12 | // 'User-Agent': 13 | // 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362' 14 | }); 15 | 16 | dio = new Dio(options); 17 | 18 | //添加拦截器 19 | dio.interceptors 20 | .add(InterceptorsWrapper(onRequest: (RequestOptions options) { 21 | // print("-----开始请求-----"); 22 | return options; 23 | }, onResponse: (Response response) { 24 | // print("-----开始响应-----"); 25 | return response; 26 | }, onError: (DioError e) { 27 | // print("-----发生错误-----"); 28 | return e; 29 | })); 30 | } 31 | 32 | /* 33 | * get请求 34 | */ 35 | Future get(url, {data, options}) async { 36 | dio.options.responseType = ResponseType.plain; 37 | dio.options.contentType = "application/x-www-form-urlencoded"; 38 | try { 39 | Response response = 40 | await dio.get(url, queryParameters: data, options: options); 41 | return response.data.toString(); 42 | } on DioError catch (e) { 43 | if (e != null && e.response != null) { 44 | if (e.response.statusCode == 301) { 45 | var re = await get(e.response.headers['location'][0], 46 | options: options, data: data); 47 | return re; 48 | } 49 | if (e.response.statusCode == 302) { 50 | var re = await get(e.response.headers['location'][0], 51 | options: options, data: data); 52 | return re; 53 | } 54 | } 55 | return formatError(e); 56 | } 57 | } 58 | 59 | /* 60 | * post请求 61 | */ 62 | Future post(url, {data, options}) async { 63 | dio.options.responseType = ResponseType.plain; 64 | dio.options.contentType = "application/x-www-form-urlencoded"; 65 | try { 66 | Response response = 67 | await dio.post(url, queryParameters: data, options: options); 68 | return response.data.toString(); 69 | } on DioError catch (e) { 70 | if (e != null && e.response != null) { 71 | if (e.response.statusCode == 301) { 72 | var re = await post(e.response.headers['location'][0], 73 | options: options, data: data); 74 | return re; 75 | } 76 | if (e.response.statusCode == 302) { 77 | var re = await post(e.response.headers['location'][0], 78 | options: options, data: data); 79 | return re; 80 | } 81 | } 82 | return formatError(e); 83 | } 84 | } 85 | 86 | /* 87 | * 下载文件 88 | */ 89 | Future download(String urlPath, String savePath, 90 | {void Function(int, int) progress}) async { 91 | try { 92 | double before = 0; 93 | await dio.download(urlPath, savePath, 94 | options: Options(receiveTimeout: 0), 95 | onReceiveProgress: progress ?? 96 | (int count, int total) { 97 | double position = (count / total) * 100; 98 | if (position - before > 5) { 99 | print('下载进度---> ${position.round()}% '); 100 | before = position; 101 | } 102 | }); 103 | return true; 104 | } on DioError { 105 | return false; 106 | } 107 | } 108 | 109 | /* 110 | * error统一处理 111 | */ 112 | String formatError(DioError e) { 113 | return "请求失败"; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/app/tool/random.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | //生成定长随机数 4 | String randomCodeX(int strlenght) { 5 | String alphabet = '0123456789'; 6 | String left = ''; 7 | for (var i = 0; i < strlenght; i++) { 8 | left = left + alphabet[Random().nextInt(alphabet.length)]; 9 | } 10 | return left; 11 | } 12 | 13 | //生成时间戳 14 | String randomCode(int strlenght) => '${DateTime.now().millisecondsSinceEpoch}'; 15 | 16 | randomListItem(List temp) => temp[Random().nextInt(temp.length)]; 17 | 18 | List copyList(List temp) => 19 | List.generate(temp.length, (int index) => temp[index], growable: true); 20 | 21 | List randomList(List order) { 22 | List old = copyList(order); 23 | int index = 0; 24 | while (old.length > 1) { 25 | var e = randomListItem(old); 26 | order[index] = e; 27 | old.remove(e); 28 | index++; 29 | } 30 | return order; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /lib/app/tool/search.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:feiyu/app/model/movie.dart'; 4 | import 'package:feiyu/app/model/site.dart'; 5 | import 'package:feiyu/app/tool/http.dart'; 6 | 7 | import 'package:xml2json/xml2json.dart'; 8 | 9 | class Search { 10 | static Future> api(String keyword, Site site, 11 | {bool force = true}) async => 12 | spiderAPI(keyword, site, force: force); 13 | 14 | static Future> web(String keyword, Site site, 15 | {bool force = true}) async => 16 | spiderWeb(keyword, site, force: force); 17 | 18 | static Future> spiderAPI(String keyword, Site site, 19 | {bool force = true}) async { 20 | dynamic temp; 21 | Http http = Http(); 22 | List movies = []; 23 | String text = await http.get(site.xml + "?wd=" + keyword); 24 | try { 25 | temp = getUrlsApi(text, site); 26 | } catch (e) { 27 | return movies; 28 | } 29 | List links = temp['links']; 30 | List titles = temp['titles']; 31 | if (links.length < 1) return movies; 32 | for (var link in links) { 33 | var title = titles[links.indexOf(link)].trim(); 34 | //严格模式 35 | if (force && !title.contains(keyword)) continue; 36 | //获取详情 37 | text = await http.get(site.xml + '?ac=videolist&ids=' + link); 38 | try { 39 | temp = getM3u8sApi(text, site); 40 | } catch (e) { 41 | continue; 42 | } 43 | List m3u8s = temp['m3u8s']; 44 | List items = temp['items']; 45 | String cover = 'http' + (site.server + temp['cover']).split('http').last; 46 | String desp = temp['desp']; 47 | List playlists = []; 48 | for (var m3u8 in m3u8s) { 49 | playlists.add(Playlist(m3u8: m3u8, name: items[m3u8s.indexOf(m3u8)])); 50 | } 51 | movies.add( 52 | Movie(title: title, cover: cover, desp: desp, playlist: playlists)); 53 | } 54 | return movies; 55 | } 56 | 57 | static getM3u8sApi(String text, Site site) { 58 | String cover, desp; 59 | List m3u8s, items = [], m3u8x = []; 60 | var video = jsonDecode(xmlToJson(text))['rss']['list']['video']; 61 | desp = video['des']; 62 | cover = video['pic']; 63 | m3u8s = video['dl']['dd'].split('#'); 64 | for (var m3u8 in m3u8s) { 65 | if (m3u8.contains(r'$')) { 66 | items.add(m3u8.split(r'$')[0]); 67 | m3u8x.add(m3u8.split(r'$')[1]); 68 | } 69 | } 70 | return {"m3u8s": m3u8x, "cover": cover, "desp": desp, "items": items}; 71 | } 72 | 73 | static getUrlsApi(String text, Site site) { 74 | List titles = [], links = []; 75 | var json = jsonDecode(xmlToJson(text)); 76 | if (json['rss']['list'] == null) { 77 | //搜索结果为空 78 | return {"titles": [], "links": []}; 79 | } 80 | var videos = json['rss']['list']['video']; 81 | if (!(json['rss']['list']['video'] is List)) { 82 | //只搜到一个 83 | return { 84 | "titles": [videos['name'] + '(' + videos['note'] + ')'], 85 | "links": [videos['id']] 86 | }; 87 | } 88 | //搜到多个 89 | for (var video in videos) { 90 | titles.add(video['name'] + '(' + video['note'] + ')'); 91 | links.add(video['id']); 92 | } 93 | return {"titles": titles, "links": links}; 94 | } 95 | 96 | static String xmlToJson(String xml) { 97 | var myTransformer = Xml2Json(); 98 | try { 99 | myTransformer.parse(xml); 100 | } catch (e) { 101 | return '{"rss":{"list":null}}'; 102 | } 103 | return myTransformer.toParker(); 104 | } 105 | 106 | static Future> spiderWeb(String keyword, Site site, 107 | {bool force = true}) async { 108 | Http http = Http(); 109 | List movies = []; 110 | String text = await http.post(site.server + "index.php?m=vod-search", 111 | data: {"wd": keyword, "submit": "search"}); 112 | dynamic temp = getUrlsWeb(text, site); 113 | List links = temp['links']; 114 | List titles = temp['titles']; 115 | if (links.length < 1 || links.length != titles.length) return movies; 116 | for (var link in links) { 117 | var title = titles[links.indexOf(link)].trim(); 118 | //严格模式 119 | if (force && !title.contains(keyword)) continue; 120 | //获取详情 121 | text = await http.get((site.server + link) 122 | .replaceAll('//?', '/?') 123 | .replaceAll(site.server * 2, site.server)); 124 | temp = getM3u8sWeb(text, site); 125 | List m3u8s = temp['m3u8s']; 126 | List items = temp['items']; 127 | if (temp['cover'].length < 1 || temp['desp'].length < 1) continue; 128 | String cover = 129 | 'http' + (site.server + temp['cover'][0]).split('http').last; 130 | String desp = temp['desp'][0]; 131 | List playlists = []; 132 | for (var m3u8 in m3u8s) { 133 | playlists.add(Playlist(m3u8: m3u8, name: items[m3u8s.indexOf(m3u8)])); 134 | } 135 | movies.add( 136 | Movie(title: title, cover: cover, desp: desp, playlist: playlists)); 137 | } 138 | return movies; 139 | } 140 | 141 | static getM3u8sWeb(String text, Site site) { 142 | List cover, m3u8s, desp, items = [], m3u8x = []; 143 | cover = findAll(text, site.cover); 144 | m3u8s = findAll(text, site.m3u8); 145 | // items = findAll(text, site.item); 146 | desp = findAll(text, site.desp); 147 | for (var m3u8 in m3u8s) { 148 | if (m3u8.contains(r'$')) { 149 | items.add(m3u8.split(r'$')[0]); 150 | m3u8x.add(m3u8.split(r'$')[1]); 151 | } 152 | } 153 | return {"m3u8s": m3u8x, "cover": cover, "desp": desp, "items": items}; 154 | } 155 | 156 | static getUrlsWeb(String text, Site site) { 157 | List titles, links; 158 | titles = findAll(text, site.title); 159 | links = findAll(text, site.link); 160 | return {"titles": titles, "links": links}; 161 | } 162 | 163 | static List findAll(String text, String exp) => 164 | RegExp(exp).allMatches(text).map((s) { 165 | if (s.groupCount < 1) return ''; 166 | return s.group(1); 167 | }).toList(); 168 | } 169 | -------------------------------------------------------------------------------- /lib/app/tool/time.dart: -------------------------------------------------------------------------------- 1 | 2 | String videoTime(int duration) { 3 | String twoDigits(int n) { 4 | if (n >= 10) return "$n"; 5 | return "0$n"; 6 | } 7 | String twoDigitHours = 8 | twoDigits((duration ~/ (1000 * 60 * 60)).remainder(24)); 9 | String twoDigitMinutes = twoDigits((duration ~/ (1000 * 60)).remainder(60)); 10 | String twoDigitSeconds = twoDigits((duration ~/ (1000)).remainder(60)); 11 | return twoDigitHours == '00' 12 | ? "$twoDigitMinutes:$twoDigitSeconds" 13 | : "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds"; 14 | } -------------------------------------------------------------------------------- /lib/app/widget/myBlurButton.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flustars/flustars.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | myBlurButton(IconData icon, {void Function() onTap}) { 7 | var screenWidth = ScreenUtil.getInstance().screenWidth; 8 | var size = screenWidth / 10*1.2; 9 | return GestureDetector( 10 | onTap: onTap ?? () {}, 11 | behavior: HitTestBehavior.translucent, 12 | child: ClipRRect( 13 | borderRadius: BorderRadius.circular(size / 2), 14 | child: BackdropFilter( 15 | filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), 16 | child: Container( 17 | alignment: Alignment.center, 18 | width: size, 19 | height: size, 20 | decoration: BoxDecoration( 21 | color: Colors.white.withOpacity(0.1), 22 | borderRadius: BorderRadius.circular(size / 2)), 23 | child: Icon(icon, size: size / 2, color: Colors.white))), 24 | )); 25 | } 26 | -------------------------------------------------------------------------------- /lib/app/widget/myBottomInput.dart: -------------------------------------------------------------------------------- 1 | import 'package:flustars/flustars.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'myBottomSheet.dart'; 5 | import 'myRoundButton.dart'; 6 | import 'myText.dart'; 7 | import 'myTextField.dart'; 8 | 9 | myBottomInput(context, {title = '请输入', hint = ':)', autoFocus = false}) async => 10 | await Navigator.push( 11 | context, 12 | MyBottomSheet( 13 | child: BottomInput( 14 | title: title, hintText: hint, autoFocus: autoFocus))); 15 | 16 | class BottomInput extends StatefulWidget { 17 | final String title; 18 | final String hintText; 19 | final bool autoFocus; 20 | 21 | BottomInput( 22 | {this.title = '请输入', this.hintText = ':)', this.autoFocus = false}); 23 | 24 | @override 25 | _BottomInputState createState() => _BottomInputState(); 26 | } 27 | 28 | class _BottomInputState extends State { 29 | String myTxt; 30 | @override 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | backgroundColor: Colors.black45, 34 | body: Column( 35 | children: [ 36 | Expanded( 37 | child: GestureDetector( 38 | child: Container( 39 | color: Colors.transparent, 40 | ), 41 | onTap: () { 42 | Navigator.pop(context); 43 | }, 44 | )), 45 | myRadiusBottom(context), 46 | ], 47 | ), 48 | ); 49 | } 50 | 51 | myRadiusBottom(context, {double height}) { 52 | if (height == null) height = ScreenUtil.getInstance().screenWidth / 20 * 12; 53 | double radius = height / 12 * 1.5; 54 | return Container( 55 | //弹窗布局 56 | width: double.infinity, 57 | height: height, //弹窗高度不能超过屏幕的一半!!! 58 | decoration: BoxDecoration( 59 | color: Colors.white, 60 | borderRadius: BorderRadiusDirectional.only( 61 | topStart: Radius.circular(radius), 62 | topEnd: Radius.circular(radius))), 63 | child: Column( 64 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 65 | children: [ 66 | myText(widget.title, size: height / 12 * 1), 67 | mySearchBox(context, 68 | hintText: widget.hintText, height: height / 12 * 3), 69 | Row( 70 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 71 | children: [ 72 | myRoundButton('取 消', onTap: () { 73 | Navigator.pop(context); //传回空值 74 | }, height: height / 12 * 3, colorBack: const Color(0xFFf4f5fa)), 75 | myRoundButton('确 定', onTap: () { 76 | Navigator.pop(context, myTxt); //传回输入值 77 | }, 78 | height: height / 12 * 3, 79 | colorText: Colors.lightBlue, 80 | colorBack: const Color(0xFFf4f5fa)), 81 | ], 82 | ) 83 | ], 84 | )); 85 | } 86 | 87 | mySearchBox(context, {String hintText, double height}) { 88 | //底部搜索框 89 | return MyTextField( 90 | autofocus: widget.autoFocus, 91 | height: height, 92 | hintText: hintText, 93 | onSubmit: (txt) { 94 | Navigator.pop(context, txt); //传回输入值 95 | }, 96 | onChanged: (txt) { 97 | myTxt = txt; //传回输入值 98 | }, 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/app/widget/myBottomSheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MyBottomSheet extends PopupRoute { 4 | final Duration _duration = Duration(milliseconds: 300); 5 | Widget child; 6 | 7 | MyBottomSheet({@required this.child}); 8 | 9 | @override 10 | Color get barrierColor => null; 11 | 12 | @override 13 | bool get barrierDismissible => true; 14 | 15 | @override 16 | String get barrierLabel => null; 17 | 18 | @override 19 | Widget buildPage(BuildContext context, Animation animation, 20 | Animation secondaryAnimation) { 21 | return child; 22 | } 23 | 24 | @override 25 | Duration get transitionDuration => _duration; 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /lib/app/widget/myBottomTip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flustars/flustars.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'myBottomSheet.dart'; 5 | import 'myRoundButton.dart'; 6 | 7 | myBottomTip(context, {String title, String desp}) async => await Navigator.push( 8 | context, MyBottomSheet(child: MyBottomTip(title, desp))); 9 | 10 | class MyBottomTip extends StatefulWidget { 11 | final String title, desp; 12 | MyBottomTip(this.title, this.desp); 13 | @override 14 | _MyBottomTipState createState() => _MyBottomTipState(); 15 | } 16 | 17 | class _MyBottomTipState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | backgroundColor: Colors.black45, 22 | body: Column( 23 | children: [ 24 | Expanded( 25 | child: GestureDetector( 26 | child: Container( 27 | color: Colors.transparent, 28 | ), 29 | onTap: () { 30 | Navigator.pop(context); 31 | }, 32 | )), 33 | myBottomTipLayout(context), 34 | ], 35 | )); 36 | } 37 | 38 | myBottomTipLayout(context) { 39 | double radius = ScreenUtil.getInstance().screenWidth / 20 * 1.5; 40 | return Container( 41 | //弹窗布局 42 | width: ScreenUtil.getInstance().screenWidth, 43 | decoration: BoxDecoration( 44 | color: Colors.white, 45 | borderRadius: BorderRadiusDirectional.only( 46 | topStart: Radius.circular(radius), 47 | topEnd: Radius.circular(radius))), 48 | child: Column( 49 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 50 | children: [ 51 | myHead(radius), 52 | Container( 53 | padding: EdgeInsets.only( 54 | left: radius, right: radius, bottom: radius, top: radius / 2), 55 | child: Text( 56 | widget.desp, 57 | maxLines: 20, 58 | overflow: TextOverflow.ellipsis, 59 | style: TextStyle(fontSize: 14), 60 | ), 61 | ) 62 | ], 63 | )); 64 | } 65 | 66 | myHead(radius) => Container( 67 | padding: EdgeInsets.only( 68 | left: radius / 2, right: radius / 2, top: radius / 3), 69 | child: Row( 70 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 71 | children: [ 72 | myRoundButton('', 73 | colorBack: Colors.transparent, 74 | width: ScreenUtil.getInstance().screenWidth / 20 * 2, 75 | height: ScreenUtil.getInstance().screenWidth / 20 * 2), 76 | Text( 77 | widget.title, 78 | maxLines: 10, 79 | style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), 80 | ), 81 | myRoundButton('', 82 | width: ScreenUtil.getInstance().screenWidth / 20 * 2, 83 | height: ScreenUtil.getInstance().screenWidth / 20 * 2, 84 | icon: Icons.close, 85 | colorBack: const Color(0xFFf4f5fa), onTap: () { 86 | Navigator.pop(context); 87 | }) 88 | ], 89 | ), 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /lib/app/widget/myImage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | //封装图片加载控件,增加图片加载失败时加载默认图片 4 | class MyImage extends StatefulWidget { 5 | final String url; 6 | final BoxFit fit; 7 | final Widget error; 8 | final Widget load; 9 | final double width; 10 | final double height; 11 | 12 | MyImage(this.url, 13 | {this.width, 14 | this.height, 15 | this.fit = BoxFit.cover, 16 | this.error = const Placeholder(), 17 | this.load = const Placeholder()}); 18 | 19 | @override 20 | State createState() { 21 | return _StateMyImage(); 22 | } 23 | } 24 | 25 | class _StateMyImage extends State { 26 | Widget _image; 27 | 28 | @override 29 | void setState(fn) { 30 | if (mounted) { 31 | super.setState(fn); 32 | } 33 | } 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | _image = widget.load; 39 | Image _imagex = Image.network( 40 | widget.url, 41 | width: widget.width, 42 | height: widget.height, 43 | fit: widget.fit, 44 | ); 45 | var resolve = _imagex.image.resolve(ImageConfiguration.empty); 46 | resolve.addListener(ImageStreamListener((_, __) { 47 | //加载成功 48 | setState(() { 49 | _image = _imagex; 50 | }); 51 | }, onChunk: (ImageChunkEvent event) { 52 | //加载中 53 | setState(() { 54 | _image = widget.load; 55 | }); 56 | }, onError: (dynamic exception, StackTrace stackTrace) { 57 | //加载失败 58 | setState(() { 59 | _image = widget.error; 60 | }); 61 | })); 62 | } 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | return _image; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/app/widget/myRoundButton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'myText.dart'; 3 | 4 | myRoundButton(String txt, 5 | {IconData icon, 6 | void Function() onTap, 7 | double height = 60, 8 | double size, 9 | double width, 10 | double radius, 11 | Color colorText = Colors.black, 12 | Color colorBack = const Color(0xFFf4f5fa)}) { 13 | return GestureDetector( 14 | onTap: onTap ?? 15 | () { 16 | print('点击了$txt'); 17 | }, 18 | child: Container( 19 | //弹窗布局 20 | width: width ?? height * 2 / 0.7, 21 | height: height, 22 | decoration: BoxDecoration( 23 | color: colorBack, 24 | borderRadius: BorderRadiusDirectional.all( 25 | Radius.circular(radius ?? height / 2))), 26 | child: Row( 27 | mainAxisAlignment: MainAxisAlignment.center, 28 | children: [ 29 | icon != null 30 | ? Icon(icon, color: colorText, size: size ?? height / 3) 31 | : SizedBox(width: 0, height: 0), 32 | myText(txt, color: colorText, size: size ?? height / 3), 33 | ], 34 | )), 35 | ); 36 | } 37 | 38 | myRoundText(String txt, 39 | {IconData icon, 40 | void Function() onTap, 41 | double height = 60, 42 | double width, 43 | double radius, 44 | double size, 45 | Color colorText = Colors.black, 46 | Color colorBack = Colors.white, 47 | List gradient}) { 48 | return GestureDetector( 49 | onTap: onTap ?? 50 | () { 51 | print('点击了$txt'); 52 | }, 53 | child: Container( 54 | //弹窗布局 55 | width: width ?? height * 2 / 0.7, 56 | height: height, 57 | alignment: Alignment.center, 58 | padding: EdgeInsets.symmetric(horizontal: radius ?? height / 2), 59 | decoration: BoxDecoration( 60 | gradient: LinearGradient( 61 | begin: Alignment.topLeft, 62 | end: Alignment.bottomRight, 63 | colors: gradient ?? [colorBack, colorBack], 64 | ), 65 | borderRadius: BorderRadiusDirectional.all( 66 | Radius.circular(radius ?? height / 2))), 67 | child: myText(txt, color: colorText, size: size ?? height / 3), 68 | ), 69 | ); 70 | } 71 | 72 | myIcon(IconData icon, 73 | {void Function() onTap, 74 | double sizeBack = 40, 75 | double sizeIcon = 20, 76 | Color colorIcon = Colors.black, 77 | Color colorBack = Colors.transparent}) { 78 | return GestureDetector( 79 | behavior: HitTestBehavior.opaque, 80 | onTap: onTap ?? () {}, 81 | child: Container( 82 | width: sizeBack, 83 | height: sizeBack, 84 | decoration: BoxDecoration( 85 | color: colorBack, 86 | borderRadius: 87 | BorderRadiusDirectional.all(Radius.circular(sizeBack / 2))), 88 | child: Icon( 89 | icon, 90 | size: sizeIcon, 91 | color: colorIcon, 92 | ), 93 | )); 94 | } 95 | -------------------------------------------------------------------------------- /lib/app/widget/myText.dart: -------------------------------------------------------------------------------- 1 | import 'package:flustars/flustars.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | Widget myText(String txt, 5 | {FontWeight big = FontWeight.w400, 6 | Color color = Colors.black, 7 | double size = 16, 8 | double lSpace = 1, 9 | double wSpace = 1, 10 | TextOverflow overflow = TextOverflow.ellipsis}) { 11 | return Text(txt, 12 | overflow: overflow, 13 | style: TextStyle( 14 | letterSpacing: lSpace, 15 | wordSpacing: wSpace, 16 | fontSize: size, 17 | color: color, 18 | fontWeight: big, 19 | decoration: TextDecoration.none, //禁用字体下划线 20 | )); 21 | } 22 | 23 | Widget myLable(String lable) { 24 | return Row( 25 | children: [ 26 | SizedBox(width: ScreenUtil.getInstance().screenWidth / 20), 27 | Icon( 28 | Icons.label, 29 | color: Colors.yellow, 30 | ), 31 | SizedBox(width: ScreenUtil.getInstance().screenWidth / 20), 32 | myText(lable, color: Colors.black, size: 18, big: FontWeight.bold) 33 | ], 34 | ); 35 | } 36 | 37 | -------------------------------------------------------------------------------- /lib/app/widget/myTextField.dart: -------------------------------------------------------------------------------- 1 | import 'package:flustars/flustars.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class MyTextField extends StatefulWidget { 5 | final Null Function(String) onSubmit; 6 | final Null Function(String) onChanged; 7 | final Function() focused; 8 | final Function() nofocus; 9 | final String hintText; 10 | final String value; 11 | final double height; 12 | final double width; 13 | final Color colorText; 14 | final Color colorBack; 15 | final bool autofocus; 16 | final TextAlign textAlign; 17 | final TextEditingController controller; 18 | 19 | MyTextField( 20 | {this.onSubmit, 21 | this.onChanged, 22 | this.focused, 23 | this.nofocus, 24 | this.hintText = '请输入...', 25 | this.height, 26 | this.width, 27 | this.textAlign = TextAlign.center, 28 | this.colorBack = const Color(0xFFf4f5fa), 29 | this.colorText = const Color(0xFFa8b3cf), 30 | this.autofocus = false, 31 | this.value, 32 | this.controller}); 33 | 34 | @override 35 | _MyTextFieldState createState() => _MyTextFieldState(); 36 | } 37 | 38 | class _MyTextFieldState extends State { 39 | double height; 40 | double width; 41 | TextEditingController controller = TextEditingController(); 42 | bool isFocused = false; 43 | FocusNode focusNode = FocusNode(); 44 | bool firstBuild = true; 45 | String nowValue; 46 | 47 | @override 48 | void initState() { 49 | super.initState(); 50 | height = widget.height ?? ScreenUtil.getInstance().screenWidth/20 * 3; 51 | width = widget.width ?? ScreenUtil.getInstance().screenWidth - ScreenUtil.getInstance().screenWidth/20 * 2; 52 | focusNode.addListener(() { 53 | if (focusNode.hasFocus) { 54 | isFocused = true; 55 | if (widget.focused != null) widget.focused(); 56 | print('聚焦'); 57 | setState(() {}); 58 | } else { 59 | isFocused = false; 60 | print('失焦'); 61 | if (widget.nofocus != null) widget.nofocus(); 62 | setState(() {}); 63 | } 64 | }); 65 | if (widget.controller == null && widget.value != null) { 66 | controller.text = widget.value; 67 | } 68 | } 69 | 70 | @override 71 | void dispose() { 72 | // widget.controller?.dispose(); 73 | controller?.dispose(); 74 | focusNode?.dispose(); 75 | super.dispose(); 76 | } 77 | 78 | @override 79 | Widget build(BuildContext context) { 80 | return Container( 81 | alignment: Alignment.center, 82 | height: height, 83 | width: width, 84 | decoration: BoxDecoration( 85 | color: widget.colorBack, 86 | borderRadius: BorderRadius.all(Radius.circular(height / 2)), 87 | ), 88 | padding: EdgeInsets.symmetric(horizontal: height / 2), 89 | child: TextField( 90 | controller: widget.controller ?? controller, 91 | textAlign: widget.textAlign ?? TextAlign.center, //文字居中 92 | autofocus: widget.autofocus, //自动打开软键盘 93 | focusNode: focusNode, 94 | onChanged: widget.onChanged ?? 95 | (String txt) async { 96 | print(txt); 97 | }, 98 | onSubmitted: widget.onSubmit ?? 99 | (String txt) async { 100 | print(txt); 101 | }, 102 | keyboardType: TextInputType.text, 103 | obscureText: false, //是否输入密码 104 | textInputAction: TextInputAction.done, //完成 105 | style: TextStyle( 106 | //输入文字样式,决定光标的高度 107 | fontSize: height / 3, 108 | fontWeight: FontWeight.w500, 109 | color: Colors.black), 110 | decoration: InputDecoration( 111 | border: InputBorder.none, //无边框 112 | hintText: isFocused ? '' : widget.hintText, 113 | hintStyle: TextStyle( 114 | fontSize: height / 3, 115 | fontWeight: FontWeight.w400, 116 | color: widget.colorText), 117 | ), 118 | //光标颜色 119 | cursorColor: Colors.black, 120 | ), 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/app/widget/myTips.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | //弹窗-->消息提示 4 | 5 | Future myTips(context, title, words) async { 6 | return showDialog( 7 | context: context, 8 | child: new AlertDialog( 9 | title: Text(title,style: TextStyle(color: Colors.red),), 10 | content: Text(words), 11 | actions: [ 12 | FlatButton( 13 | onPressed: () { 14 | Navigator.of(context).pop(); 15 | }, 16 | child: Text("确定",style: TextStyle(color: Colors.blue))), 17 | ], 18 | )); 19 | } 20 | -------------------------------------------------------------------------------- /lib/app/widget/myToast.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:fluttertoast/fluttertoast.dart'; 5 | 6 | void myToast(String name) { 7 | Fluttertoast.showToast( 8 | msg: name, 9 | toastLength: Toast.LENGTH_SHORT, 10 | gravity: ToastGravity.CENTER, 11 | backgroundColor: Colors.blue, 12 | textColor: Colors.white, 13 | fontSize: 12.0); 14 | } -------------------------------------------------------------------------------- /lib/generated_plugin_registrant.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // ignore: unused_import 6 | import 'dart:ui'; 7 | 8 | import 'package:fluttertoast/fluttertoast_web.dart'; 9 | import 'package:shared_preferences_web/shared_preferences_web.dart'; 10 | import 'package:video_player_web/video_player_web.dart'; 11 | 12 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 13 | 14 | void registerPlugins(PluginRegistry registry) { 15 | FluttertoastWebPlugin.registerWith(registry.registrarFor(FluttertoastWebPlugin)); 16 | SharedPreferencesPlugin.registerWith(registry.registrarFor(SharedPreferencesPlugin)); 17 | VideoPlayerPlugin.registerWith(registry.registrarFor(VideoPlayerPlugin)); 18 | registry.registerMessageHandler(); 19 | } 20 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:feiyu/pages/home.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | void main() { 6 | runApp(MyApp()); 7 | //透明状态栏 8 | SystemUiOverlayStyle style = SystemUiOverlayStyle( 9 | statusBarColor: Colors.transparent, 10 | systemNavigationBarDividerColor: null, 11 | systemNavigationBarColor: Colors.transparent, 12 | systemNavigationBarIconBrightness: Brightness.light, 13 | statusBarIconBrightness: Brightness.light, 14 | ); 15 | SystemChrome.setSystemUIOverlayStyle(style); 16 | } 17 | 18 | class MyApp extends StatelessWidget { 19 | @override 20 | Widget build(BuildContext context) { 21 | return MaterialApp( 22 | title: '飞鱼', 23 | theme: ThemeData( 24 | primaryColor: Colors.white, 25 | ), 26 | home: Home()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/pages/detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:feiyu/app/model/movie.dart'; 2 | import 'package:feiyu/app/model/player.dart'; 3 | import 'package:feiyu/app/widget/myBlurButton.dart'; 4 | import 'package:feiyu/app/widget/myImage.dart'; 5 | import 'package:feiyu/app/widget/myRoundButton.dart'; 6 | import 'package:feiyu/app/widget/myText.dart'; 7 | import 'package:feiyu/pages/playx.dart'; 8 | import 'package:feiyu/pages/share.dart'; 9 | import 'package:flustars/flustars.dart'; 10 | import 'package:flutter/material.dart'; 11 | 12 | class Detail extends StatefulWidget { 13 | final Movie movie; 14 | final Player player; 15 | Detail(this.movie, {@required this.player}); 16 | @override 17 | _DetailState createState() => _DetailState(this.movie); 18 | } 19 | 20 | class _DetailState extends State { 21 | Movie movie; 22 | Playlist selected; 23 | 24 | double screenWidth = ScreenUtil.getInstance().screenWidth; 25 | // 屏幕高 26 | double screenHeight = ScreenUtil.getInstance().screenHeight; 27 | _DetailState(this.movie); 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | selected = movie.playlist[0]; 33 | } 34 | 35 | void share() async { 36 | await Navigator.push( 37 | context, 38 | MaterialPageRoute( 39 | builder: (BuildContext context) => Share( 40 | title: movie.title, 41 | cover: movie.cover, 42 | m3u8: selected.m3u8, 43 | name: selected.name, 44 | player: widget.player), 45 | ), 46 | ); 47 | } 48 | 49 | void play() async { 50 | await Navigator.push( 51 | context, 52 | MaterialPageRoute( 53 | builder: (BuildContext context) => 54 | PlayX(movie, selected, player: widget.player), 55 | ), 56 | ); 57 | //从播放页返回时直接返回上一页 58 | Navigator.pop(context); 59 | } 60 | 61 | Widget header() { 62 | return Container( 63 | padding: EdgeInsets.all(screenWidth / 20), 64 | child: Row( 65 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 66 | children: [ 67 | myBlurButton(Icons.keyboard_backspace, onTap: () { 68 | Navigator.pop(context); 69 | }), 70 | myBlurButton(Icons.share, onTap: share), 71 | ], 72 | )); 73 | } 74 | 75 | Widget myItem(Playlist playlist) { 76 | var flag = selected == playlist; 77 | return GestureDetector( 78 | onTap: () { 79 | setState(() { 80 | //播放 81 | selected = playlist; 82 | }); 83 | play(); 84 | }, 85 | child: Container( 86 | padding: EdgeInsets.all(10), 87 | decoration: BoxDecoration( 88 | color: flag ? Colors.yellow : const Color(0xFFf4f5fa), 89 | borderRadius: BorderRadiusDirectional.all(Radius.circular(10))), 90 | child: myText(playlist.name, 91 | size: 12, 92 | color: flag ? Colors.black : Colors.black, 93 | big: flag ? FontWeight.bold : null)), 94 | ); 95 | } 96 | 97 | @override 98 | Widget build(BuildContext context) { 99 | return Scaffold( 100 | backgroundColor: Colors.white, 101 | body: Stack(children: [ 102 | //背景图 103 | Hero( 104 | tag: movie.cover + 'bg', 105 | child: Container( 106 | color: Colors.black54, 107 | width: screenWidth, 108 | child: MyImage( 109 | movie.cover, 110 | fit: BoxFit.cover, 111 | ), 112 | )), 113 | ListView( 114 | children: [ 115 | Container( 116 | width: screenWidth, 117 | height: screenWidth * 1.6 - screenWidth * 0.15, 118 | child: Stack(children: [ 119 | //标题 120 | Positioned( 121 | top: screenWidth * 1 - screenWidth * 0.0, 122 | left: screenWidth / 10 + screenWidth / 3, 123 | child: Container( 124 | color: Colors.black54, 125 | width: screenWidth * 0.52 + 20, 126 | padding: EdgeInsets.symmetric(horizontal: 10), 127 | child: Text('${movie.title}', 128 | maxLines: 2, 129 | overflow: TextOverflow.ellipsis, 130 | style: TextStyle( 131 | color: Colors.white, 132 | fontSize: 26, 133 | fontWeight: FontWeight.bold, 134 | )), 135 | )), 136 | //异形底部 137 | Positioned( 138 | top: screenWidth - screenWidth * 0.0, 139 | child: ClipPath( 140 | clipper: ArcClipper(), 141 | child: Container( 142 | width: screenWidth, 143 | height: screenWidth * 0.3, 144 | color: Colors.white, 145 | ), 146 | )), 147 | //底部背景色 148 | Positioned( 149 | top: screenWidth + screenWidth * 0.3 - screenWidth * 0.0, 150 | child: Container( 151 | width: screenWidth, 152 | height: screenWidth / 6, 153 | color: Colors.white, 154 | child: SizedBox()), 155 | ), 156 | //播放按钮 157 | Positioned( 158 | top: screenWidth * 1.21 - screenWidth * 0.0, 159 | left: screenWidth / 20 * 13, 160 | child: Material( 161 | elevation: 18, 162 | shadowColor: Colors.yellowAccent, 163 | color: Colors.transparent, 164 | borderRadius: BorderRadiusDirectional.all( 165 | Radius.circular(screenWidth / 20 * 3 / 2)), 166 | child: myIcon(Icons.play_arrow, 167 | onTap: play, 168 | colorIcon: Colors.white, 169 | colorBack: Colors.yellow, 170 | sizeIcon: screenWidth / 20 * 2, 171 | sizeBack: screenWidth / 20 * 3), 172 | ), 173 | ), 174 | //完整封面 175 | Positioned( 176 | top: screenWidth * 1 - screenWidth * 0.0, 177 | left: screenWidth / 10, 178 | child: Material( 179 | elevation: 10, 180 | child: Hero( 181 | tag: movie.cover, 182 | child: Container( 183 | width: screenWidth / 4, 184 | height: screenWidth / 4 / 0.7, 185 | child: MyImage( 186 | movie.cover, 187 | fit: BoxFit.cover, 188 | ), 189 | ))), 190 | ), 191 | ]), 192 | ), 193 | Container( 194 | color: Colors.white, 195 | child: Column( 196 | children: [ 197 | myLable('选集'), 198 | SizedBox(height: screenWidth / 20), 199 | Container( 200 | padding: 201 | EdgeInsets.symmetric(horizontal: screenWidth / 10), 202 | child: Wrap( 203 | spacing: 10, 204 | runSpacing: 10, 205 | children: 206 | movie.playlist.map((e) => myItem(e)).toList(), 207 | ), 208 | ), 209 | SizedBox(height: screenWidth / 20), 210 | myLable('简介'), 211 | SizedBox(height: screenWidth / 20), 212 | Container( 213 | padding: 214 | EdgeInsets.symmetric(horizontal: screenWidth / 10), 215 | child: Text( 216 | movie.desp, 217 | maxLines: 10, 218 | overflow: TextOverflow.ellipsis, 219 | style: TextStyle( 220 | color: Colors.black, 221 | fontSize: 14, 222 | letterSpacing: 1.5), 223 | ), 224 | ), 225 | SizedBox(height: screenWidth / 10), 226 | ], 227 | )), 228 | ], 229 | ), 230 | SafeArea(child: header()), 231 | ])); 232 | } 233 | } 234 | 235 | class ArcClipper extends CustomClipper { 236 | @override 237 | Path getClip(Size size) { 238 | var path = Path(); 239 | path.lineTo(0.0, size.height); 240 | path.lineTo(size.width, size.height); 241 | var firstControlPoint = Offset(size.width / 3, size.height); 242 | var firstPoint = Offset(0, 0); 243 | path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy, 244 | firstPoint.dx, firstPoint.dy); 245 | path.close(); 246 | 247 | return path; 248 | } 249 | 250 | @override 251 | bool shouldReclip(CustomClipper oldClipper) => false; 252 | } 253 | -------------------------------------------------------------------------------- /lib/pages/home.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:ui'; 4 | import 'package:sensors/sensors.dart'; 5 | 6 | import 'package:feiyu/app/config/config.dart'; 7 | import 'package:feiyu/app/model/movie.dart'; 8 | import 'package:feiyu/app/model/player.dart'; 9 | import 'package:feiyu/app/model/push.dart'; 10 | import 'package:feiyu/app/model/site.dart'; 11 | import 'package:feiyu/app/tool/http.dart'; 12 | import 'package:feiyu/app/tool/random.dart'; 13 | import 'package:feiyu/app/widget/myBlurButton.dart'; 14 | import 'package:feiyu/app/widget/myBottomInput.dart'; 15 | import 'package:feiyu/app/widget/myBottomTip.dart'; 16 | import 'package:feiyu/app/widget/myImage.dart'; 17 | import 'package:feiyu/app/widget/myText.dart'; 18 | import 'package:feiyu/app/widget/myTips.dart'; 19 | import 'package:feiyu/pages/searchList.dart'; 20 | import 'package:flustars/flustars.dart'; 21 | import 'package:flutter/material.dart'; 22 | 23 | import 'detail.dart'; 24 | 25 | class Home extends StatefulWidget { 26 | final Http http; 27 | final Push push; 28 | final Player player; 29 | final List sites; 30 | final List movies; 31 | final bool loading; 32 | 33 | Home( 34 | {this.http, 35 | this.push, 36 | this.player, 37 | this.sites, 38 | this.movies, 39 | this.loading}); 40 | 41 | @override 42 | _HomeState createState() => _HomeState(); 43 | } 44 | 45 | class _HomeState extends State { 46 | Http http; 47 | double screenWidth, screenHeight; 48 | 49 | Push push; 50 | Player player; 51 | List sites = []; 52 | List movies = []; 53 | bool loading = true; 54 | 55 | List _accelerometerValues; 56 | List _userAccelerometerValues; 57 | List _gyroscopeValues; 58 | List> _streamSubscriptions = 59 | >[]; 60 | 61 | @override 62 | void dispose() { 63 | super.dispose(); 64 | for (StreamSubscription subscription in _streamSubscriptions) { 65 | subscription.cancel(); 66 | } 67 | } 68 | 69 | @override 70 | void initState() { 71 | super.initState(); 72 | screenWidth = ScreenUtil.getInstance().screenWidth; 73 | screenHeight = ScreenUtil.getInstance().screenHeight; 74 | _streamSubscriptions 75 | .add(accelerometerEvents.listen((AccelerometerEvent event) { 76 | setState(() { 77 | _accelerometerValues = [event.x, event.y, event.z]; 78 | }); 79 | })); 80 | _streamSubscriptions.add(gyroscopeEvents.listen((GyroscopeEvent event) { 81 | setState(() { 82 | _gyroscopeValues = [event.x, event.y, event.z]; 83 | }); 84 | })); 85 | _streamSubscriptions 86 | .add(userAccelerometerEvents.listen((UserAccelerometerEvent event) { 87 | setState(() { 88 | _userAccelerometerValues = [event.x, event.y, event.z]; 89 | }); 90 | })); 91 | if (widget.movies == null) { 92 | Future.delayed(Duration.zero, () async { 93 | await load(context); //获取推送通知 94 | }); 95 | } else { 96 | reopen(); 97 | setState(() {}); 98 | } 99 | } 100 | 101 | sensor() { 102 | final List accelerometer = 103 | _accelerometerValues?.map((double v) => v.toStringAsFixed(1))?.toList(); 104 | final List gyroscope = 105 | _gyroscopeValues?.map((double v) => v.toStringAsFixed(1))?.toList(); 106 | final List userAccelerometer = _userAccelerometerValues 107 | ?.map((double v) => v.toStringAsFixed(1)) 108 | ?.toList(); 109 | return Column( 110 | children: [ 111 | Text('Accelerometer: $accelerometer'), 112 | Text('gyroscope: $gyroscope'), 113 | Text('userAccelerometer: $userAccelerometer'), 114 | ], 115 | ); 116 | } 117 | 118 | reopen() { 119 | http = widget.http; 120 | push = widget.push; 121 | player = widget.player; 122 | sites = widget.sites; 123 | movies = widget.movies; 124 | loading = widget.loading; 125 | } 126 | 127 | Future load(context) async { 128 | http = Http(); 129 | //获取推送 130 | String result = await http.get('$SERVER/$APP/push.json'); 131 | if (result.contains('请求失败')) { 132 | push = Push.fromJson(jsonDecode(PUSH)); 133 | await myTips(context, "通知", push.info); 134 | if (push.force.contains('是')) return; 135 | } else { 136 | push = Push.fromJson(jsonDecode(result)); 137 | if (push.flag.contains('开')) { 138 | await myTips(context, "通知", push.info); 139 | if (push.force.contains('是')) return; 140 | } 141 | } 142 | //获取网页播放器 143 | result = await http.get('$SERVER/$APP/player.json'); 144 | if (result.contains('请求失败')) { 145 | player = Player.fromJson(jsonDecode(PLAYER)); 146 | } else { 147 | player = Player.fromJson(jsonDecode(result)); 148 | } 149 | //获取首页电影 150 | result = await http.get('$SERVER/$APP/movies.json'); 151 | if (result.contains('请求失败')) { 152 | movies = jsonDecode(MOVIES).map((e) => Movie.fromJson(e)).toList(); 153 | } else { 154 | movies = jsonDecode(result).map((e) => Movie.fromJson(e)).toList(); 155 | } 156 | //获取资源站 157 | result = await http.get('$SERVER/$APP/sites.json'); 158 | if (result.contains('请求失败')) { 159 | sites = jsonDecode(SITES).map((e) => Site.fromJson(e)).toList(); 160 | } else { 161 | sites = jsonDecode(result).map((e) => Site.fromJson(e)).toList(); 162 | } 163 | loading = false; 164 | //随机显示电影 165 | movies = randomMovies(movies); 166 | setState(() {}); 167 | } 168 | 169 | void about() async { 170 | await myBottomTip(context, 171 | title: '关于', 172 | desp: push?.about ?? 173 | '飞鱼是一个极简的播放器,它是我最近的一个Flutter项目,代码完全开源,仅供学习交流,想要了解更多信息,请关注公众号:乂乂又又。'); 174 | } 175 | 176 | void search() async { 177 | var s = await myBottomInput(context, 178 | title: '搜索', hint: '请输入电影名称...', autoFocus: true); 179 | if (s != null && s.length > 0) { 180 | await Navigator.push( 181 | context, 182 | MaterialPageRoute( 183 | builder: (BuildContext context) => 184 | SearchList(s, player: player, sites: sites), 185 | ), 186 | ); 187 | Navigator.of(context).pushReplacement(MaterialPageRoute( 188 | builder: (BuildContext context) => Home( 189 | http: http, 190 | push: push, 191 | player: player, 192 | sites: sites, 193 | movies: randomMovies(movies), 194 | loading: loading, 195 | ), 196 | )); 197 | } 198 | } 199 | 200 | void play(Movie movie) async { 201 | await Navigator.push( 202 | context, 203 | MaterialPageRoute( 204 | builder: (BuildContext context) => Detail(movie, player: player), 205 | ), 206 | ); 207 | } 208 | 209 | var max = 10; 210 | var big = 0.2; 211 | Widget moviePage(Movie movie) { 212 | return GestureDetector( 213 | behavior: HitTestBehavior.translucent, 214 | onTap: () { 215 | play(movie); 216 | }, 217 | child: Stack( 218 | children: [ 219 | AnimatedPositioned( 220 | left: screenWidth * big / 2 / max * _accelerometerValues[0], 221 | right: 222 | screenWidth * big / 2 / max * _accelerometerValues[0] * -1, 223 | top: 224 | screenHeight * big / 2 / max * _accelerometerValues[1] * -1, 225 | bottom: screenHeight * big / 2 / max * _accelerometerValues[1], 226 | duration: const Duration(milliseconds: 90), 227 | curve: Curves.linear, 228 | child: Hero( 229 | tag: movie.cover, 230 | child: Container( 231 | width: screenWidth * (1 + big), 232 | height: screenHeight * (1 + big), 233 | child: MyImage(movie.cover, fit: BoxFit.cover), 234 | ))), 235 | Align( 236 | alignment: Alignment.center, 237 | child: Container( 238 | width: screenWidth, 239 | height: screenHeight, 240 | alignment: Alignment.center, 241 | padding: EdgeInsets.all(screenWidth / 20), 242 | child: Column( 243 | mainAxisAlignment: MainAxisAlignment.center, 244 | children: [ 245 | Expanded(flex: 1, child: SizedBox(width: 0, height: 0)), 246 | // sensor(), 247 | Container( 248 | margin: EdgeInsets.all(screenWidth / 20), 249 | child: Icon(Icons.play_circle_filled, 250 | size: 64, color: Colors.white), 251 | ), 252 | Expanded(flex: 1, child: SizedBox(width: 0, height: 0)), 253 | GestureDetector( 254 | behavior: HitTestBehavior.translucent, 255 | onTap: () async { 256 | await myBottomTip(context, 257 | title: movie.title, desp: movie.desp); 258 | }, 259 | child: Icon(Icons.keyboard_arrow_up, 260 | size: screenWidth / 10 * 1, color: Colors.white)), 261 | GestureDetector( 262 | behavior: HitTestBehavior.translucent, 263 | onTap: () async { 264 | await myBottomTip(context, 265 | title: movie.title, desp: movie.desp); 266 | }, 267 | child: ClipRRect( 268 | borderRadius: 269 | BorderRadius.circular(screenWidth / 20), 270 | child: BackdropFilter( 271 | filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), 272 | child: Container( 273 | padding: EdgeInsets.only( 274 | bottom: screenWidth / 20, 275 | left: screenWidth / 20, 276 | right: screenWidth / 20, 277 | top: screenWidth / 20 / 2), 278 | decoration: BoxDecoration( 279 | color: Colors.white.withOpacity(0.1), 280 | borderRadius: BorderRadius.circular( 281 | screenWidth / 20)), 282 | child: Column( 283 | mainAxisSize: MainAxisSize.min, 284 | crossAxisAlignment: 285 | CrossAxisAlignment.center, 286 | children: [ 287 | Text( 288 | movie.title, 289 | maxLines: 1, 290 | overflow: TextOverflow.ellipsis, 291 | style: TextStyle( 292 | color: Colors.white, 293 | fontSize: 20, 294 | fontWeight: FontWeight.bold), 295 | ), 296 | SizedBox(height: 5), 297 | Text( 298 | movie.desp, 299 | maxLines: 3, 300 | overflow: TextOverflow.ellipsis, 301 | style: TextStyle( 302 | color: Colors.white, 303 | fontSize: 12), 304 | ) 305 | ], 306 | ))))), 307 | ], 308 | ), 309 | ), 310 | ) 311 | ], 312 | )); 313 | } 314 | 315 | Widget header() { 316 | return Container( 317 | padding: EdgeInsets.all(screenWidth / 20), 318 | child: Row( 319 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 320 | children: [ 321 | myBlurButton(Icons.sort, onTap: about), 322 | myBlurButton(Icons.search, onTap: search), 323 | ], 324 | )); 325 | } 326 | 327 | Widget loadingPage() { 328 | return Container( 329 | width: screenWidth, 330 | color: Colors.white, 331 | child: Column( 332 | children: [ 333 | Expanded(child: SizedBox()), 334 | CircularProgressIndicator(), 335 | SizedBox(height: 24), 336 | myText('初始化...', color: Colors.black), 337 | Expanded(child: SizedBox()), 338 | ], 339 | )); 340 | } 341 | 342 | @override 343 | Widget build(BuildContext context) { 344 | return Scaffold( 345 | backgroundColor: Colors.white, 346 | body: Container( 347 | color: Colors.black54, 348 | child: loading 349 | ? loadingPage() 350 | : Stack(children: [ 351 | OverflowBox( 352 | maxWidth: screenWidth * (1 + big), 353 | maxHeight: screenHeight * (1 + big), 354 | child: PageView( 355 | physics: BouncingScrollPhysics(), 356 | scrollDirection: Axis.vertical, 357 | children: movies 358 | .map((movie) => moviePage(movie)) 359 | .toList(), 360 | )), 361 | SafeArea(child: header()), 362 | ]), 363 | )); 364 | } 365 | 366 | List randomMovies(List old) { 367 | int len = old.length; 368 | List newList = new List()..length = len; 369 | List order = old.map((e) => old.indexOf(e)).toList()..sort(); 370 | for (var i = 0; i < len; i++) { 371 | var index = randomListItem(order); 372 | newList[i] = old[index]; 373 | order.remove(index); 374 | } 375 | return newList; 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /lib/pages/play.dart: -------------------------------------------------------------------------------- 1 | import 'package:feiyu/app/model/movie.dart'; 2 | import 'package:feiyu/app/widget/myBottomTip.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'video/video_player_UI.dart'; 5 | 6 | class Play extends StatelessWidget { 7 | final Playlist playlist; 8 | Play(this.playlist); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | backgroundColor: Colors.black, 14 | body: VideoPlayerUI.network( 15 | url: playlist.m3u8, 16 | title: playlist.name, 17 | share: () async { 18 | await myBottomTip(context, 19 | title: '关于', desp: '飞鱼是一个极简的播放器,它是我最近的一个Flutter项目。'); 20 | }, 21 | full: (bool full) async { 22 | await myBottomTip(context, 23 | title: '关于', desp: full ? '全屏--》未全屏' : '未全屏--》全屏'); 24 | }, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/pages/playx.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:feiyu/app/model/movie.dart'; 3 | import 'package:feiyu/app/model/player.dart'; 4 | import 'package:feiyu/app/widget/myBottomSheet.dart'; 5 | import 'package:feiyu/app/widget/myText.dart'; 6 | import 'package:feiyu/pages/share.dart'; 7 | import 'package:feiyu/pages/tv.dart'; 8 | import 'package:flustars/flustars.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:screen/screen.dart'; 11 | import 'package:video_player/video_player.dart'; 12 | import 'package:feiyu/pages/video/controller_widget.dart'; 13 | import 'package:feiyu/pages/video/video_player_control.dart'; 14 | import 'package:feiyu/pages/video/video_player_pan.dart'; 15 | 16 | class PlayX extends StatefulWidget { 17 | final Movie movie; 18 | final Playlist playlist; 19 | final Player player; 20 | PlayX(this.movie, this.playlist, {@required this.player}); 21 | 22 | @override 23 | _PlayXState createState() => _PlayXState(); 24 | } 25 | 26 | class _PlayXState extends State { 27 | Playlist selected; 28 | final GlobalKey _key = 29 | GlobalKey(); 30 | 31 | ///指示video资源是否加载完成,加载完成后会获得总时长和视频长宽比等信息 32 | bool _videoInit = false; 33 | bool _videoError = false; 34 | bool changing = false; 35 | 36 | VideoPlayerController _controller; // video控件管理器 37 | 38 | /// 记录是否全屏 39 | bool get _isFullScreen => 40 | MediaQuery.of(context).orientation == Orientation.landscape; 41 | 42 | Size get _window => MediaQueryData.fromWindow(window).size; 43 | 44 | DateTime _tempTime = DateTime.now(); 45 | 46 | share() async { 47 | if (changing) return; 48 | await _controller?.pause(); 49 | await Navigator.push( 50 | context, 51 | MaterialPageRoute( 52 | builder: (BuildContext context) => Share( 53 | title: widget.movie.title, 54 | cover: widget.movie.cover, 55 | m3u8: selected.m3u8, 56 | name: selected.name, 57 | player: widget.player), 58 | ), 59 | ); 60 | await _controller?.play(); 61 | } 62 | 63 | shareTV() async { 64 | if (changing) return; 65 | await _controller?.pause(); 66 | await Navigator.push(context, MyBottomSheet(child: TV(playlist: selected))); 67 | await _controller?.play(); 68 | } 69 | 70 | full(bool fullx) { 71 | setState(() {}); //刷新界面 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | var screen = ScreenUtil.getInstance(); 77 | var screenWidth = screen.screenWidth; 78 | return Scaffold( 79 | backgroundColor: Colors.white, 80 | body: ListView(padding: EdgeInsets.zero, children: [ 81 | Offstage( 82 | offstage: _isFullScreen, 83 | child: SafeArea( 84 | child: AppBar( 85 | elevation: 0, 86 | centerTitle: true, 87 | title: Text( 88 | widget.movie.title, 89 | overflow: TextOverflow.ellipsis, 90 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), 91 | ), 92 | actions: [ 93 | IconButton( 94 | icon: Icon(Icons.share), 95 | onPressed: share, 96 | ), 97 | ], 98 | ))), 99 | Container( 100 | color: Colors.black, 101 | width: !_isFullScreen ? _window.width : _window.width, 102 | height: !_isFullScreen ? _window.width / 16 * 9 : _window.height, 103 | child: _isHadUrl(), 104 | ), 105 | Offstage( 106 | offstage: _isFullScreen, 107 | child: Container( 108 | height: _isFullScreen 109 | ? 0 110 | : _window.height - 111 | _window.width / 16 * 9 - 112 | screen.appBarHeight - 113 | screen.statusBarHeight, 114 | child: ListView( 115 | physics: BouncingScrollPhysics(), 116 | padding: EdgeInsets.zero, 117 | children: [ 118 | SizedBox(height: screenWidth / 20), 119 | myLable('选集'), 120 | SizedBox(height: screenWidth / 20), 121 | Container( 122 | alignment: Alignment.center, 123 | padding: 124 | EdgeInsets.symmetric(horizontal: screenWidth / 10), 125 | child: Wrap( 126 | spacing: 10, 127 | runSpacing: 10, 128 | children: widget.movie.playlist 129 | .map((e) => myItem(e)) 130 | .toList(), 131 | ), 132 | ), 133 | SizedBox(height: screenWidth / 20), 134 | myLable('简介'), 135 | SizedBox(height: screenWidth / 20), 136 | Container( 137 | padding: 138 | EdgeInsets.symmetric(horizontal: screenWidth / 10), 139 | child: Text( 140 | widget.movie.desp, 141 | maxLines: 10, 142 | overflow: TextOverflow.ellipsis, 143 | style: TextStyle( 144 | color: Colors.black, 145 | fontSize: 14, 146 | letterSpacing: 1.5), 147 | ), 148 | ), 149 | SizedBox(height: screenWidth / 10), 150 | ]), 151 | )), 152 | ]), 153 | ); 154 | } 155 | 156 | Widget myItem(Playlist playlist) { 157 | var flag = selected == playlist; 158 | return GestureDetector( 159 | onTap: () async { 160 | setState(() { 161 | selected = playlist; 162 | }); 163 | await _urlChange(); 164 | }, 165 | child: Container( 166 | padding: EdgeInsets.all(10), 167 | decoration: BoxDecoration( 168 | color: flag ? Colors.yellow : const Color(0xFFf4f5fa), 169 | borderRadius: BorderRadiusDirectional.all(Radius.circular(10))), 170 | child: myText(playlist.name, 171 | size: 12, 172 | color: flag ? Colors.black : Colors.black, 173 | big: flag ? FontWeight.bold : null)), 174 | ); 175 | } 176 | 177 | // 判断是否有url 178 | Widget _isHadUrl() { 179 | return ControllerWidget( 180 | controlKey: _key, 181 | controller: _controller, 182 | videoInit: _videoInit, 183 | title: selected.name, 184 | child: VideoPlayerPan( 185 | share: shareTV, 186 | full: full, 187 | child: Container( 188 | alignment: Alignment.center, 189 | width: double.infinity, 190 | height: double.infinity, 191 | color: Colors.black, 192 | child: _isVideoInit(), 193 | ), 194 | ), 195 | ); 196 | } 197 | 198 | // 加载url成功时,根据视频比例渲染播放器 199 | Widget _isVideoInit() { 200 | if (_videoInit) { 201 | if (_videoError) { 202 | return Text( 203 | '加载失败,请重试~', 204 | style: TextStyle(color: Colors.white), 205 | ); 206 | } 207 | return AspectRatio( 208 | aspectRatio: _controller?.value?.aspectRatio, 209 | child: VideoPlayer(_controller), 210 | ); 211 | } else if (_controller != null && !changing) { 212 | return Text( 213 | '加载失败,请重试~', 214 | style: TextStyle(color: Colors.white), 215 | ); 216 | } else { 217 | return Column( 218 | mainAxisAlignment: MainAxisAlignment.center, 219 | children: [ 220 | SizedBox( 221 | width: 30, 222 | height: 30, 223 | child: CircularProgressIndicator(), 224 | ), 225 | SizedBox(height: 30), 226 | Text( 227 | '加载中...', 228 | style: TextStyle(color: Colors.white), 229 | ) 230 | ], 231 | ); 232 | } 233 | } 234 | 235 | void _videoListener() async { 236 | if (_controller?.value?.hasError ?? false) { 237 | _videoError = true; 238 | changing = false; 239 | print(_controller?.value?.errorDescription); 240 | } 241 | if (_controller?.value?.initialized ?? false) { 242 | changing = false; 243 | } 244 | if (_controller?.value?.isBuffering ?? false) {} 245 | if (DateTime.now().difference(_tempTime).inMilliseconds > 999) { 246 | // Duration res = await _controller?.position; 247 | if (_controller?.value?.isPlaying ?? false) { 248 | _key.currentState?.setPosition( 249 | position: _controller?.value?.position ?? Duration.zero, 250 | totalDuration: _controller?.value?.duration ?? Duration.zero, 251 | ); 252 | } 253 | _tempTime = DateTime.now(); 254 | } 255 | setState(() {}); 256 | } 257 | 258 | @override 259 | void initState() { 260 | super.initState(); 261 | selected = widget.playlist; 262 | _urlChange(); // 初始进行一次url加载 263 | Screen.keepOn(true); // 设置屏幕常亮 264 | } 265 | 266 | @override 267 | void dispose() { 268 | super.dispose(); 269 | _controller?.removeListener(_videoListener); 270 | _controller?.dispose(); 271 | Screen.keepOn(false); 272 | } 273 | 274 | _urlChange() async { 275 | setState(() { 276 | changing = true; 277 | }); 278 | var old = _controller; 279 | if (_controller != null) { 280 | /// 如果控制器存在,清理掉重新创建 281 | _controller?.removeListener(_videoListener); 282 | await _controller?.pause(); 283 | } 284 | _controller = VideoPlayerController.network(selected.m3u8); 285 | setState(() { 286 | /// 重置组件参数 287 | _videoInit = false; 288 | _videoError = false; 289 | }); 290 | 291 | /// 加载资源完成时,监听播放进度,并且标记_videoInit=true加载完成 292 | _controller?.addListener(_videoListener); 293 | try { 294 | await _controller?.initialize(); 295 | _key.currentState?.setPosition( 296 | position: Duration(seconds: 0), 297 | totalDuration: _controller?.value?.duration ?? Duration(seconds: 0), 298 | ); 299 | setState(() { 300 | _videoInit = true; 301 | _videoError = false; 302 | _controller?.play(); 303 | }); 304 | await old?.dispose(); 305 | } catch (e) { 306 | setState(() { 307 | _videoInit = true; 308 | _videoError = true; 309 | _controller?.pause(); 310 | }); 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /lib/pages/searchList.dart: -------------------------------------------------------------------------------- 1 | import 'package:feiyu/app/model/movie.dart'; 2 | import 'package:feiyu/app/model/player.dart'; 3 | import 'package:feiyu/app/model/site.dart'; 4 | import 'package:feiyu/app/tool/search.dart'; 5 | import 'package:feiyu/app/widget/myBottomInput.dart'; 6 | import 'package:feiyu/app/widget/myImage.dart'; 7 | import 'package:feiyu/app/widget/myText.dart'; 8 | import 'package:flustars/flustars.dart'; 9 | import 'package:flutter/material.dart'; 10 | 11 | import 'detail.dart'; 12 | 13 | class SearchList extends StatefulWidget { 14 | final String title; 15 | final Player player; 16 | final List sites; 17 | SearchList(this.title, {@required this.sites, @required this.player}); 18 | @override 19 | _SearchListState createState() => _SearchListState(); 20 | } 21 | 22 | class _SearchListState extends State 23 | with SingleTickerProviderStateMixin { 24 | TabController _tabController; 25 | List _tabs = []; 26 | bool loading = false; 27 | bool api = true; 28 | 29 | List movies = []; 30 | String keyword = ''; 31 | List> tabMovies = []; 32 | double screenWidth = ScreenUtil.getInstance().screenWidth; 33 | double screenHeight = ScreenUtil.getInstance().screenHeight; 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | keyword = widget.title; 39 | _tabs = widget.sites.map((site) => site.name).toList(); 40 | _tabs.sort(); //排序 41 | tabMovies = new List>(_tabs.length); 42 | //tab切换controller 43 | _tabController = new TabController(vsync: this, length: _tabs.length); 44 | setState(() {}); //刷新界面 45 | setListener(); 46 | search(); //首次搜索 47 | } 48 | 49 | @override 50 | void dispose() { 51 | super.dispose(); 52 | _tabController.dispose(); 53 | } 54 | 55 | void setListener() { 56 | _tabController.addListener(() async { 57 | var i = _tabController.index; 58 | //监听页面被切换时 59 | if (tabMovies[i] != null) { 60 | //再次切换 61 | movies = tabMovies[i]; 62 | setState(() {}); 63 | return; 64 | } 65 | //首次切换 66 | await search(); 67 | }); 68 | 69 | ///监听TabController的动画,实时刷新,这样选中背景就能跟随移动了 70 | _tabController.animation.addListener(() { 71 | if (!(_tabController.animation.value % 1 == 0)) { 72 | loading = true; 73 | } else { 74 | loading = false; 75 | } 76 | setState(() {}); 77 | }); 78 | } 79 | 80 | Future search() async { 81 | loading = true; 82 | setState(() {}); 83 | String siteName = _tabs[_tabController.index]; 84 | if (api) { 85 | movies = await Search.api(keyword, 86 | widget.sites[widget.sites.indexWhere((s) => s.name == siteName)]); 87 | } else { 88 | movies = await Search.web(keyword, 89 | widget.sites[widget.sites.indexWhere((s) => s.name == siteName)]); 90 | } 91 | tabMovies[_tabController.index] = movies; 92 | loading = false; 93 | setState(() {}); 94 | } 95 | 96 | onTapSearch() async { 97 | keyword = ''; 98 | var s = await myBottomInput(context, 99 | title: '搜索', hint: '请输入电影名称...', autoFocus: true); 100 | if (s != null) { 101 | keyword = s; 102 | //重置搜索结果 103 | tabMovies = new List>(_tabs.length); 104 | await search(); 105 | } 106 | } 107 | 108 | void play(Movie movie) async { 109 | Navigator.push( 110 | context, 111 | MaterialPageRoute( 112 | builder: (BuildContext context) => Detail(movie, player: widget.player), 113 | ), 114 | ); 115 | } 116 | 117 | Widget myMovieItem(Movie movie) { 118 | return GestureDetector( 119 | behavior: HitTestBehavior.translucent, 120 | onTap: () { 121 | play(movie); 122 | }, 123 | child: Container( 124 | margin: EdgeInsets.all(screenWidth / 20 * 0.5), 125 | child: ListTile( 126 | leading: Hero( 127 | tag: movie.cover + 'bg', 128 | child: Container( 129 | width: screenWidth / 20 * 2.4, 130 | height: screenWidth / 20 * 4, 131 | child: MyImage(movie.cover, fit: BoxFit.cover))), 132 | title: Text( 133 | movie.title, 134 | maxLines: 1, 135 | overflow: TextOverflow.ellipsis, 136 | style: TextStyle( 137 | color: Colors.black, fontSize: 15, fontWeight: FontWeight.bold), 138 | ), 139 | subtitle: Text( 140 | movie.desp, 141 | maxLines: 2, 142 | overflow: TextOverflow.ellipsis, 143 | style: TextStyle(color: Colors.black54, fontSize: 12), 144 | ), 145 | trailing: Container( 146 | margin: EdgeInsets.all(screenWidth / 20), 147 | child: 148 | Icon(Icons.play_circle_filled, size: 32, color: Colors.yellow), 149 | ), 150 | ), 151 | ), 152 | ); 153 | } 154 | 155 | Widget loadingPage() { 156 | return Container( 157 | width: screenWidth, 158 | color: Colors.white, 159 | child: Column( 160 | children: [ 161 | Expanded(child: SizedBox()), 162 | Offstage( 163 | offstage: !loading, 164 | child: CircularProgressIndicator(), 165 | ), 166 | SizedBox(height: 24), 167 | myText(loading ? '搜索中...' : '啥都没搜到~', color: Colors.black), 168 | Expanded(flex: 2, child: SizedBox()), 169 | ], 170 | )); 171 | } 172 | 173 | Widget myBody() { 174 | return (loading || movies.length < 1) 175 | ? loadingPage() 176 | : RefreshIndicator( 177 | onRefresh: search, 178 | child: ListView.builder( 179 | physics: BouncingScrollPhysics(), 180 | itemCount: movies.length, 181 | itemBuilder: (BuildContext context, int index) { 182 | if (index == movies.length - 1) 183 | return Container( 184 | height: screenHeight, 185 | alignment: Alignment.topCenter, 186 | child: myMovieItem(movies[index])); 187 | return myMovieItem(movies[index]); 188 | })); 189 | } 190 | 191 | @override 192 | Widget build(BuildContext context) { 193 | return DefaultTabController( 194 | length: _tabs.length, // This is the number of tabs. 195 | child: Scaffold( 196 | appBar: AppBar( 197 | elevation: 0, 198 | centerTitle: true, 199 | title: myText(keyword, 200 | size: 18, overflow: TextOverflow.ellipsis, big: FontWeight.bold), 201 | actions: [ 202 | IconButton( 203 | icon: myText(api ? 'A' : 'W', size: 16, big: FontWeight.bold), 204 | onPressed: () async { 205 | setState(() { 206 | api = !api; 207 | }); 208 | //重新搜索 209 | await search(); 210 | }) 211 | ], 212 | bottom: TabBar( 213 | isScrollable: true, 214 | indicatorColor: Colors.yellow, 215 | indicatorSize: TabBarIndicatorSize.label, 216 | indicatorPadding: EdgeInsets.all(20), 217 | indicatorWeight: 10, 218 | labelColor: Colors.black, 219 | labelStyle: TextStyle(fontWeight: FontWeight.bold), 220 | unselectedLabelColor: Colors.black54, 221 | controller: this._tabController, 222 | tabs: _tabs.map((String name) => Tab(text: name)).toList(), 223 | ), 224 | ), 225 | body: TabBarView( 226 | controller: this._tabController, 227 | children: _tabs.map((String name) { 228 | return SafeArea( 229 | top: false, 230 | bottom: false, 231 | child: Builder( 232 | builder: (BuildContext context) { 233 | return myBody(); 234 | }, 235 | ), 236 | ); 237 | }).toList(), 238 | ), 239 | floatingActionButton: FloatingActionButton( 240 | backgroundColor: Colors.black, 241 | onPressed: onTapSearch, 242 | child: Icon(Icons.search,color: Colors.white), 243 | ), 244 | ), 245 | ); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /lib/pages/share.dart: -------------------------------------------------------------------------------- 1 | import 'package:feiyu/app/model/player.dart'; 2 | import 'package:feiyu/app/tool/decode.dart'; 3 | import 'package:feiyu/app/widget/myImage.dart'; 4 | import 'package:feiyu/app/widget/myText.dart'; 5 | import 'package:feiyu/app/widget/myToast.dart'; 6 | import 'package:flustars/flustars.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter/services.dart'; 9 | import 'package:qr_flutter/qr_flutter.dart'; 10 | 11 | class Share extends StatelessWidget { 12 | final Player player; //在线播放器 13 | final String title; //标题 14 | final String cover; //封面 15 | final String name; //剧集 16 | final String m3u8; 17 | 18 | Share({this.title, this.name, this.m3u8, this.cover, this.player}); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | var screenWidth = ScreenUtil.getInstance().screenWidth; 23 | var screenHeight = ScreenUtil.getInstance().screenHeight; 24 | var wh = 1 / 0.7; 25 | var imgW = screenWidth * 0.6; 26 | var imgH = imgW * wh; 27 | var pSize = screenWidth / 20; 28 | var pBottom = (screenHeight - imgH - imgW) / 2 + imgW - pSize; 29 | var pMargin = (screenWidth - imgW) / 2 - pSize; 30 | var pColor = Colors.yellow; 31 | var flag = player.public.contains('是'); 32 | var qrstr = 33 | '${player.server}?${player.m3u8}=${flag ? m3u8 : encodex(m3u8)}&${player.title}=${flag ? title : encodex(title)}'; 34 | return Scaffold( 35 | backgroundColor: Colors.yellow, 36 | body: Container( 37 | child: Stack( 38 | children: [ 39 | //返回 40 | Align( 41 | alignment: Alignment.topLeft, 42 | child: SafeArea( 43 | child: IconButton( 44 | iconSize: 36, 45 | padding: EdgeInsets.all(screenWidth / 20), 46 | icon: Icon( 47 | Icons.keyboard_backspace, 48 | color: Colors.white, 49 | size: 36, 50 | ), 51 | onPressed: () { 52 | Navigator.pop(context); 53 | })), 54 | ), 55 | //分享 56 | Align( 57 | alignment: Alignment.topRight, 58 | child: SafeArea( 59 | child: IconButton( 60 | iconSize: 36, 61 | padding: EdgeInsets.all(screenWidth / 20), 62 | icon: Icon( 63 | Icons.share, 64 | color: Colors.white, 65 | size: 36, 66 | ), 67 | onPressed: () { 68 | Clipboard.setData( 69 | ClipboardData(text: '$title $name\n播放地址:$qrstr')); 70 | myToast('已复制'); 71 | })), 72 | ), 73 | //body 74 | Align( 75 | alignment: Alignment.center, 76 | child: Column( 77 | // shrinkWrap: true, 78 | mainAxisAlignment: MainAxisAlignment.center, 79 | children: [ 80 | ClipRRect( 81 | borderRadius: BorderRadius.circular(pSize / 2), 82 | child: Container( 83 | width: imgW, height: imgH, child: MyImage(cover))), 84 | Container( 85 | width: imgW, 86 | height: imgW, 87 | padding: EdgeInsets.all(screenWidth / 20), 88 | decoration: BoxDecoration( 89 | color: Colors.white, 90 | borderRadius: BorderRadius.circular(pSize / 2)), 91 | child: Column( 92 | mainAxisSize: MainAxisSize.min, 93 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 94 | children: [ 95 | myText(title, big: FontWeight.bold, size: 16), 96 | QrImage( 97 | data: qrstr, 98 | version: QrVersions.auto, 99 | size: screenWidth * 0.36, 100 | errorStateBuilder: (cxt, err) { 101 | return Container( 102 | child: Center( 103 | child: Text( 104 | "糟糕,出错了...", 105 | textAlign: TextAlign.center, 106 | ), 107 | ), 108 | ); 109 | }, 110 | ), 111 | myText('扫码播放→' + name, 112 | big: FontWeight.bold, size: 14), 113 | ]), 114 | ) 115 | ], 116 | ), 117 | ), 118 | Positioned( 119 | bottom: pBottom, 120 | left: pMargin, 121 | child: Container( 122 | width: pSize * 2, 123 | height: pSize * 2, 124 | decoration: BoxDecoration( 125 | color: pColor, 126 | borderRadius: BorderRadius.circular(pSize)), 127 | )), 128 | Positioned( 129 | bottom: pBottom, 130 | right: pMargin, 131 | child: Container( 132 | width: pSize * 2, 133 | height: pSize * 2, 134 | decoration: BoxDecoration( 135 | color: pColor, 136 | borderRadius: BorderRadius.circular(pSize)), 137 | )) 138 | ], 139 | ), 140 | ), 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/pages/tv.dart: -------------------------------------------------------------------------------- 1 | import 'package:dlna/dlna.dart'; 2 | import 'package:feiyu/app/model/movie.dart'; 3 | import 'package:feiyu/app/widget/myRoundButton.dart'; 4 | import 'package:feiyu/app/widget/myText.dart'; 5 | import 'package:feiyu/app/widget/myToast.dart'; 6 | import 'package:flustars/flustars.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | class TV extends StatefulWidget { 10 | final Playlist playlist; 11 | 12 | TV({this.playlist}); 13 | @override 14 | _TVState createState() => _TVState(); 15 | } 16 | 17 | class _TVState extends State { 18 | bool searching = true; 19 | var screenWidth = ScreenUtil.getInstance().screenWidth; 20 | var screenHeight = ScreenUtil.getInstance().screenHeight; 21 | 22 | DLNAManager dlnaManager; 23 | List _devices = []; 24 | VideoObject _didlObject; 25 | DLNADevice _dlnaDevice; 26 | String actionMessage = ''; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | Future.delayed(Duration.zero).then((value) async => await init()); 32 | } 33 | 34 | Future init() async { 35 | //初始化DLNAManager 36 | dlnaManager = DLNAManager(); 37 | // dlnaManager.enableCache(); 38 | //设置视频对象 39 | _didlObject = VideoObject( 40 | widget.playlist?.name ?? '爱的迫降', 41 | widget.playlist?.m3u8 ?? 42 | 'https://txxs.mahua-yongjiu.com/20191214/7845_6fcff130/index.m3u8', 43 | VideoObject.VIDEO_MP4); 44 | _didlObject.refreshPosition = true; 45 | //监听DLNAManager 46 | dlnaManager.setRefresher(DeviceRefresher(onDeviceAdd: (dlnaDevice) { 47 | if (!_devices.contains(dlnaDevice)) { 48 | print('add ' + dlnaDevice.toString()); 49 | _devices.add(dlnaDevice); 50 | searching = false; 51 | } 52 | setState(() {}); 53 | }, onDeviceRemove: (dlnaDevice) { 54 | print('remove ' + dlnaDevice.toString()); 55 | _devices.remove(dlnaDevice); 56 | setState(() {}); 57 | }, onDeviceUpdate: (dlnaDevice) { 58 | print('update ' + dlnaDevice.toString()); 59 | setState(() {}); 60 | }, onSearchError: (error) { 61 | print('error ' + error); 62 | }, onPlayProgress: (positionInfo) { 63 | print(_time2Str(DateTime.now().millisecondsSinceEpoch) + 64 | ' current play progress ' + 65 | positionInfo.relTime); 66 | })); 67 | // 开始搜索 68 | dlnaManager.startSearch(); 69 | setState(() {}); 70 | } 71 | 72 | play(index) async { 73 | //选择设备 74 | _dlnaDevice = _devices[index]; 75 | dlnaManager.setDevice(_dlnaDevice); 76 | //设置链接 77 | var result = await dlnaManager.actSetVideoUrl(_didlObject); 78 | if (!result.success) { 79 | myToast('投屏失败'); 80 | Navigator.pop(context); 81 | } 82 | //播放 83 | var re = await dlnaManager.actPlay(); 84 | if (!re.success) { 85 | myToast('投屏失败'); 86 | Navigator.pop(context); 87 | } 88 | myToast('投屏成功'); 89 | Navigator.pop(context); 90 | } 91 | 92 | @override 93 | void dispose() { 94 | super.dispose(); 95 | dlnaManager?.release(); //关闭连接 96 | } 97 | 98 | @override 99 | Widget build(BuildContext context) { 100 | var flag = screenHeight <= screenWidth; 101 | return Scaffold( 102 | backgroundColor: Colors.black54, 103 | body: GestureDetector( 104 | behavior: HitTestBehavior.translucent, 105 | onTap: () { 106 | Navigator.pop(context); 107 | }, 108 | child: Container( 109 | color: Colors.black54, 110 | child: Stack( 111 | children: [ 112 | //body 113 | Align( 114 | alignment: Alignment.center, 115 | child: Material( 116 | borderRadius: BorderRadius.circular(screenWidth / 20), 117 | child: GestureDetector( 118 | behavior: HitTestBehavior.translucent, 119 | onTap: () {}, 120 | child: Container( 121 | width: 122 | (flag ? screenHeight * 1 : screenWidth) * 0.8, 123 | height: (flag ? screenHeight * 0.8 : screenWidth) * 124 | (0.8 / 9 * 11), 125 | padding: EdgeInsets.all( 126 | (flag ? screenHeight : screenWidth) / 20), 127 | decoration: BoxDecoration( 128 | color: Colors.white, 129 | borderRadius: 130 | BorderRadius.circular(screenWidth / 20)), 131 | child: Column( 132 | children: [ 133 | myText('投屏', big: FontWeight.bold), 134 | Container( 135 | padding: EdgeInsets.all( 136 | (flag ? screenHeight : screenWidth) / 20), 137 | child: Text( 138 | "*请选择一个要投屏的设备", 139 | textAlign: TextAlign.center, 140 | style: TextStyle( 141 | color: Colors.orange, 142 | ), 143 | ), 144 | ), 145 | Expanded( 146 | child: searching || _devices.length < 1 147 | ? searchW() 148 | : ListView.builder( 149 | padding: EdgeInsets.zero, 150 | physics: BouncingScrollPhysics(), 151 | itemCount: _devices.length, 152 | itemBuilder: (_, index) { 153 | return GestureDetector( 154 | behavior: 155 | HitTestBehavior.translucent, 156 | onTap: () async { 157 | await play(index); 158 | }, 159 | child: ListTile( 160 | title: Text( 161 | _devices[index].deviceName, 162 | textAlign: TextAlign.center, 163 | style: TextStyle( 164 | fontSize: 14, 165 | fontWeight: 166 | FontWeight.bold), 167 | ), 168 | trailing: myIcon( 169 | Icons.play_circle_filled, 170 | colorIcon: Colors.yellow, 171 | onTap: () async { 172 | await play(index); 173 | }), 174 | ), 175 | ); 176 | })), 177 | SizedBox( 178 | height: 179 | (flag ? screenHeight : screenWidth) / 180 | 20), 181 | Row( 182 | mainAxisAlignment: 183 | MainAxisAlignment.spaceAround, 184 | children: [ 185 | myRoundButton('取消', onTap: () { 186 | Navigator.pop(context); 187 | }, 188 | colorText: Colors.red, 189 | size: 16, 190 | height: 36), 191 | myRoundButton('确定', onTap: () { 192 | Navigator.pop(context); 193 | }, 194 | colorText: Colors.blue, 195 | size: 16, 196 | height: 36) 197 | ], 198 | ) 199 | ], 200 | ), 201 | ))), 202 | ), 203 | ], 204 | ), 205 | )), 206 | ); 207 | } 208 | 209 | Widget searchW() { 210 | return Center( 211 | child: Container( 212 | child: Column( 213 | mainAxisAlignment: MainAxisAlignment.center, 214 | children: [ 215 | SizedBox( 216 | width: 30, 217 | height: 30, 218 | child: CircularProgressIndicator(), 219 | ), 220 | SizedBox(height: 30), 221 | Text( 222 | '搜索中...', 223 | style: TextStyle(color: Colors.black), 224 | ) 225 | ], 226 | ), 227 | ), 228 | ); 229 | } 230 | 231 | String _time2Str(int intTime) { 232 | var time = DateTime.fromMillisecondsSinceEpoch(intTime); 233 | return "${time.year.toString()}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}"; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /lib/pages/video/after_layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | mixin AfterLayoutMixin on State { 4 | @override 5 | void initState() { 6 | super.initState(); 7 | WidgetsBinding.instance 8 | .addPostFrameCallback((_) => afterFirstLayout(context)); 9 | } 10 | 11 | void afterFirstLayout(BuildContext context); 12 | } -------------------------------------------------------------------------------- /lib/pages/video/controller_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:video_player/video_player.dart'; 3 | 4 | import 'video_player_control.dart'; 5 | 6 | class ControllerWidget extends InheritedWidget { 7 | ControllerWidget({ 8 | this.controlKey, 9 | this.child, 10 | this.controller, 11 | this.videoInit, 12 | this.title 13 | }); 14 | 15 | final String title; 16 | final GlobalKey controlKey; 17 | final Widget child; 18 | final VideoPlayerController controller; 19 | final bool videoInit; 20 | 21 | //定义一个便捷方法,方便子树中的widget获取共享数据 22 | static ControllerWidget of(BuildContext context) { 23 | return context.dependOnInheritedWidgetOfExactType(); 24 | } 25 | 26 | @override 27 | bool updateShouldNotify(InheritedWidget oldWidget) { 28 | return false; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /lib/pages/video/video_player_UI.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:screen/screen.dart'; 5 | import 'package:video_player/video_player.dart'; 6 | import 'controller_widget.dart'; 7 | import 'video_player_control.dart'; 8 | import 'video_player_pan.dart'; 9 | 10 | enum VideoPlayerType { network, asset, file } 11 | 12 | class VideoPlayerUI extends StatefulWidget { 13 | final Function share; 14 | final Function(bool) full; 15 | VideoPlayerUI.network({ 16 | Key key, 17 | this.share, 18 | this.full, 19 | @required String url, // 当前需要播放的地址 20 | this.width: double.infinity, // 播放器尺寸(大于等于视频播放区域) 21 | this.height: double.infinity, 22 | this.title = '', // 视频需要显示的标题 23 | }) : type = VideoPlayerType.network, 24 | url = url, 25 | super(key: key); 26 | 27 | VideoPlayerUI.asset({ 28 | Key key, 29 | this.share, 30 | this.full, 31 | @required String dataSource, // 当前需要播放的地址 32 | this.width: double.infinity, // 播放器尺寸(大于等于视频播放区域) 33 | this.height: double.infinity, 34 | this.title = '', // 视频需要显示的标题 35 | }) : type = VideoPlayerType.asset, 36 | url = dataSource, 37 | super(key: key); 38 | 39 | VideoPlayerUI.file({ 40 | Key key, 41 | this.share, 42 | this.full, 43 | @required File file, // 当前需要播放的地址 44 | this.width: double.infinity, // 播放器尺寸(大于等于视频播放区域) 45 | this.height: double.infinity, 46 | this.title = '', // 视频需要显示的标题 47 | }) : type = VideoPlayerType.file, 48 | url = file, 49 | super(key: key); 50 | 51 | final url; 52 | final VideoPlayerType type; 53 | final double width; 54 | final double height; 55 | final String title; 56 | 57 | @override 58 | _VideoPlayerUIState createState() => _VideoPlayerUIState(); 59 | } 60 | 61 | class _VideoPlayerUIState extends State { 62 | final GlobalKey _key = 63 | GlobalKey(); 64 | 65 | ///指示video资源是否加载完成,加载完成后会获得总时长和视频长宽比等信息 66 | bool _videoInit = false; 67 | bool _videoError = false; 68 | 69 | VideoPlayerController _controller; // video控件管理器 70 | 71 | /// 记录是否全屏 72 | bool get _isFullScreen => 73 | MediaQuery.of(context).orientation == Orientation.landscape; 74 | 75 | Size get _window => MediaQueryData.fromWindow(window).size; 76 | 77 | DateTime _tempTime = DateTime.now(); 78 | 79 | void _videoListener() async { 80 | if (_controller.value.hasError) { 81 | setState(() { 82 | _videoError = true; 83 | }); 84 | } else if (DateTime.now().difference(_tempTime).inMilliseconds > 999) { 85 | _tempTime = DateTime.now(); 86 | Duration res = await _controller.position; 87 | if (res >= _controller.value.duration) { 88 | await _controller.seekTo(Duration(seconds: 0)); 89 | await _controller.pause(); 90 | } 91 | if (_controller.value.isPlaying && _key.currentState != null) { 92 | _key.currentState.setPosition( 93 | position: res, 94 | totalDuration: _controller.value.duration, 95 | ); 96 | } 97 | } 98 | } 99 | 100 | @override 101 | void initState() { 102 | super.initState(); 103 | _urlChange(); // 初始进行一次url加载 104 | Screen.keepOn(true); // 设置屏幕常亮 105 | } 106 | 107 | @override 108 | void didUpdateWidget(VideoPlayerUI oldWidget) { 109 | if (oldWidget.url != widget.url) { 110 | _urlChange(); // url变化时重新执行一次url加载 111 | } 112 | super.didUpdateWidget(oldWidget); 113 | } 114 | 115 | @override 116 | void dispose() async { 117 | super.dispose(); 118 | if (_controller != null) { 119 | _controller.removeListener(_videoListener); 120 | _controller.dispose(); 121 | } 122 | Screen.keepOn(false); 123 | } 124 | 125 | @override 126 | Widget build(BuildContext context) { 127 | return SafeArea( 128 | top: !_isFullScreen, 129 | bottom: !_isFullScreen, 130 | left: !_isFullScreen, 131 | right: !_isFullScreen, 132 | child: Container( 133 | width: _isFullScreen ? _window.width : widget.width, 134 | height: _isFullScreen ? _window.height : widget.height, 135 | child: _isHadUrl(), 136 | ), 137 | ); 138 | } 139 | 140 | // 判断是否有url 141 | Widget _isHadUrl() { 142 | if (widget.url != null) { 143 | return ControllerWidget( 144 | controlKey: _key, 145 | controller: _controller, 146 | videoInit: _videoInit, 147 | title: widget.title, 148 | child: VideoPlayerPan( 149 | share: widget.share, 150 | full: widget.full, 151 | child: Container( 152 | alignment: Alignment.center, 153 | width: double.infinity, 154 | height: double.infinity, 155 | color: Colors.black, 156 | child: _isVideoInit(), 157 | ), 158 | ), 159 | ); 160 | } else { 161 | return Center( 162 | child: Text( 163 | '暂无视频信息', 164 | style: TextStyle(color: Colors.white), 165 | ), 166 | ); 167 | } 168 | } 169 | 170 | // 加载url成功时,根据视频比例渲染播放器 171 | Widget _isVideoInit() { 172 | if (_videoInit) { 173 | return AspectRatio( 174 | aspectRatio: _controller.value.aspectRatio, 175 | child: VideoPlayer(_controller), 176 | ); 177 | } else if (_controller != null && _videoError) { 178 | return Text( 179 | '加载出错', 180 | style: TextStyle(color: Colors.white), 181 | ); 182 | } else { 183 | return SizedBox( 184 | width: 30, 185 | height: 30, 186 | child: CircularProgressIndicator( 187 | strokeWidth: 2, 188 | ), 189 | ); 190 | } 191 | } 192 | 193 | void _urlChange() async { 194 | if (widget.url == null || widget.url == '') return; 195 | if (_controller != null) { 196 | /// 如果控制器存在,清理掉重新创建 197 | _controller.removeListener(_videoListener); 198 | _controller.dispose(); 199 | } 200 | setState(() { 201 | /// 重置组件参数 202 | _videoInit = false; 203 | _videoError = false; 204 | }); 205 | if (widget.type == VideoPlayerType.file) { 206 | _controller = VideoPlayerController.file(widget.url); 207 | } else if (widget.type == VideoPlayerType.asset) { 208 | _controller = VideoPlayerController.asset(widget.url); 209 | } else { 210 | _controller = VideoPlayerController.network(widget.url); 211 | } 212 | 213 | /// 加载资源完成时,监听播放进度,并且标记_videoInit=true加载完成 214 | _controller.addListener(_videoListener); 215 | await _controller.initialize(); 216 | setState(() { 217 | _videoInit = true; 218 | _videoError = false; 219 | _controller.play(); 220 | }); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/pages/video/video_player_control.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:auto_orientation/auto_orientation.dart'; 4 | import 'package:feiyu/app/tool/time.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:video_player/video_player.dart'; 8 | 9 | import 'controller_widget.dart'; 10 | import 'video_player_slider.dart'; 11 | 12 | class VideoPlayerControl extends StatefulWidget { 13 | final Function share; 14 | final Function(bool) full; 15 | VideoPlayerControl({ 16 | Key key, 17 | this.share, 18 | this.full, 19 | }) : super(key: key); 20 | 21 | @override 22 | VideoPlayerControlState createState() => VideoPlayerControlState(); 23 | } 24 | 25 | class VideoPlayerControlState extends State { 26 | VideoPlayerController get controller => 27 | ControllerWidget.of(context).controller; 28 | bool get videoInit => ControllerWidget.of(context).videoInit; 29 | String get title => ControllerWidget.of(context).title; 30 | // 记录video播放进度 31 | Duration _position = Duration(seconds: 0); 32 | Duration _totalDuration = Duration(seconds: 0); 33 | Timer _timer; // 计时器,用于延迟隐藏控件ui 34 | bool _hidePlayControl = true; // 控制是否隐藏控件ui 35 | double _playControlOpacity = 0; // 通过透明度动画显示/隐藏控件ui 36 | /// 记录是否全屏 37 | bool get _isFullScreen => 38 | MediaQuery.of(context).orientation == Orientation.landscape; 39 | bool lock = false; //是否锁定 40 | 41 | @override 42 | void dispose() { 43 | super.dispose(); 44 | if (_timer != null) { 45 | _timer.cancel(); 46 | } 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return GestureDetector( 52 | onDoubleTap: _playOrPause, 53 | onTap: _togglePlayControl, 54 | child: Container( 55 | width: double.infinity, 56 | height: double.infinity, 57 | color: Colors.transparent, 58 | child: WillPopScope( 59 | child: Offstage( 60 | offstage: _hidePlayControl, 61 | child: AnimatedOpacity( 62 | // 加入透明度动画 63 | opacity: _playControlOpacity, 64 | duration: Duration(milliseconds: 300), 65 | child: Column( 66 | children: [ 67 | Offstage(offstage: lock, child: _top()), 68 | _middle(), 69 | Offstage(offstage: lock, child: _bottom(context)) 70 | ], 71 | ), 72 | ), 73 | ), 74 | onWillPop: _onWillPop, 75 | ), 76 | ), 77 | ); 78 | } 79 | 80 | // 拦截返回键 81 | Future _onWillPop() async { 82 | if (_isFullScreen) { 83 | _toggleFullScreen(); 84 | return false; 85 | } 86 | return true; 87 | } 88 | 89 | // 供父组件调用刷新页面,减少父组件的build 90 | void setPosition({position, totalDuration}) { 91 | setState(() { 92 | _position = position; 93 | _totalDuration = totalDuration; 94 | }); 95 | } 96 | 97 | Widget _bottom(BuildContext context) { 98 | return Container( 99 | // 底部控件的容器 100 | width: double.infinity, 101 | height: 40, 102 | decoration: BoxDecoration( 103 | gradient: LinearGradient( 104 | // 来点黑色到透明的渐变优雅一下 105 | begin: Alignment.bottomCenter, 106 | end: Alignment.topCenter, 107 | colors: [Color.fromRGBO(0, 0, 0, .7), Color.fromRGBO(0, 0, 0, .1)], 108 | ), 109 | ), 110 | child: Row( 111 | // 加载完成时才渲染,flex布局 112 | children: [ 113 | IconButton( 114 | // 播放按钮 115 | padding: EdgeInsets.zero, 116 | iconSize: 26, 117 | icon: Icon( 118 | // 根据控制器动态变化播放图标还是暂停 119 | controller.value.isPlaying ? Icons.pause : Icons.play_arrow, 120 | color: Colors.white, 121 | ), 122 | onPressed: _playOrPause, 123 | ), 124 | Expanded( 125 | // 相当于前端的flex: 1 126 | child: VideoPlayerSlider( 127 | startPlayControlTimer: _startPlayControlTimer, 128 | timer: _timer, 129 | ), 130 | ), 131 | Container( 132 | // 播放时间 133 | margin: EdgeInsets.only(left: 10), 134 | child: Text( 135 | '${_position == null ? '00:00' : videoTime(_position.inMilliseconds)}/${_totalDuration == null ? '00:00' : videoTime(_totalDuration.inMilliseconds)}', 136 | style: TextStyle(color: Colors.white), 137 | ), 138 | ), 139 | IconButton( 140 | // 全屏/横屏按钮 141 | padding: EdgeInsets.zero, 142 | iconSize: 26, 143 | icon: Icon( 144 | // 根据当前屏幕方向切换图标 145 | _isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, 146 | color: Colors.white, 147 | ), 148 | onPressed: () { 149 | // 点击切换是否全屏 150 | _toggleFullScreen(); 151 | }, 152 | ), 153 | ], 154 | ), 155 | ); 156 | } 157 | 158 | Widget myIcon(IconData icon, {Function onTap, double size = 36}) { 159 | return Container( 160 | margin: EdgeInsets.all(10), 161 | padding: EdgeInsets.all(10), 162 | decoration: BoxDecoration( 163 | color: Colors.black54, 164 | borderRadius: BorderRadiusDirectional.all(Radius.circular(size))), 165 | child: GestureDetector( 166 | onTap: onTap ?? () {}, 167 | child: Icon( 168 | icon, 169 | size: size, 170 | color: Colors.white, 171 | ), 172 | ), 173 | ); 174 | } 175 | 176 | Widget _middle() { 177 | return Expanded( 178 | child: Row( 179 | children: [ 180 | myIcon(lock ? Icons.lock_open : Icons.lock_outline, size: 24, 181 | onTap: () { 182 | setState(() { 183 | lock = !lock; 184 | }); 185 | }), 186 | Expanded(child: SizedBox(width: 0)), 187 | Offstage( 188 | offstage: lock, 189 | child: myIcon( 190 | // 根据控制器动态变化播放图标还是暂停 191 | controller.value.isPlaying ? Icons.pause : Icons.play_arrow, 192 | onTap: _playOrPause, 193 | )), 194 | Expanded(child: SizedBox(width: 0)), 195 | Offstage( 196 | offstage: lock, 197 | child: myIcon(Icons.live_tv, 198 | size: 24, onTap: widget.share ?? () {})), 199 | ], 200 | ), 201 | ); 202 | } 203 | 204 | Widget _top() { 205 | return Container( 206 | width: double.infinity, 207 | height: 40, 208 | decoration: BoxDecoration( 209 | gradient: LinearGradient( 210 | // 来点黑色到透明的渐变优雅一下 211 | begin: Alignment.bottomCenter, 212 | end: Alignment.topCenter, 213 | colors: [Color.fromRGBO(0, 0, 0, .7), Color.fromRGBO(0, 0, 0, .1)], 214 | ), 215 | ), 216 | child: Row( 217 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 218 | children: [ 219 | //在最上层或者不是横屏则隐藏按钮 220 | ModalRoute.of(context).isFirst && !_isFullScreen 221 | ? Container() 222 | : IconButton( 223 | icon: Icon( 224 | Icons.arrow_back, 225 | color: Colors.white, 226 | ), 227 | onPressed: backPress), 228 | Text( 229 | title, 230 | style: TextStyle(color: Colors.white), 231 | ), 232 | //在最上层或者不是横屏则隐藏按钮 233 | ModalRoute.of(context).isFirst && !_isFullScreen 234 | ? Container() 235 | : IconButton( 236 | icon: Icon( 237 | Icons.arrow_back, 238 | color: Colors.transparent, 239 | ), 240 | onPressed: () {}, 241 | ), 242 | ], 243 | ), 244 | ); 245 | } 246 | 247 | void backPress() { 248 | print(_isFullScreen); 249 | // 如果是全屏,点击返回键则关闭全屏,如果不是,则系统返回键 250 | if (_isFullScreen) { 251 | _toggleFullScreen(); 252 | } else if (ModalRoute.of(context).isFirst) { 253 | SystemNavigator.pop(); 254 | } else { 255 | Navigator.pop(context); 256 | } 257 | } 258 | 259 | void _playOrPause() async { 260 | if (lock) return; 261 | 262 | /// 同样的,点击动态播放或者暂停 263 | if (videoInit) { 264 | controller.value.isPlaying 265 | ? await controller.pause() 266 | : await controller.play(); 267 | setState(() {}); //更新界面 268 | _startPlayControlTimer(); // 操作控件后,重置延迟隐藏控件的timer 269 | } 270 | } 271 | 272 | void _togglePlayControl() { 273 | setState(() { 274 | if (_hidePlayControl) { 275 | /// 如果隐藏则显示 276 | _hidePlayControl = false; 277 | _playControlOpacity = 1; 278 | _startPlayControlTimer(); // 开始计时器,计时后隐藏 279 | } else { 280 | /// 如果显示就隐藏 281 | if (_timer != null) _timer.cancel(); // 有计时器先移除计时器 282 | _playControlOpacity = 0; 283 | Future.delayed(Duration(milliseconds: 500)).whenComplete(() { 284 | _hidePlayControl = true; // 延迟500ms(透明度动画结束)后,隐藏 285 | }); 286 | } 287 | }); 288 | } 289 | 290 | void _startPlayControlTimer() { 291 | /// 计时器,用法和前端js的大同小异 292 | _timer?.cancel(); 293 | _timer = Timer(Duration(seconds: 3), () { 294 | setState(() { 295 | _playControlOpacity = 0; 296 | _hidePlayControl = true; 297 | }); 298 | }); 299 | } 300 | 301 | void _toggleFullScreen() { 302 | if (widget.full != null) widget.full(_isFullScreen); 303 | setState(() { 304 | if (_isFullScreen) { 305 | /// 如果是全屏就切换竖屏 306 | AutoOrientation.portraitAutoMode(); 307 | 308 | ///显示状态栏,与底部虚拟操作按钮 309 | SystemChrome.setEnabledSystemUIOverlays( 310 | [SystemUiOverlay.top, SystemUiOverlay.bottom]); 311 | } else { 312 | AutoOrientation.landscapeAutoMode(); 313 | 314 | ///关闭状态栏,与底部虚拟操作按钮 315 | SystemChrome.setEnabledSystemUIOverlays([]); 316 | } 317 | _startPlayControlTimer(); // 操作完控件开始计时隐藏 318 | }); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /lib/pages/video/video_player_pan.dart: -------------------------------------------------------------------------------- 1 | import 'package:feiyu/app/tool/time.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:screen/screen.dart'; 4 | 5 | //import 'package:screen/screen.dart'; 6 | import 'package:video_player/video_player.dart'; 7 | import 'package:sys_volume/flutter_volume.dart'; 8 | 9 | import 'after_layout.dart'; 10 | import 'controller_widget.dart'; 11 | import 'video_player_control.dart'; 12 | 13 | class VideoPlayerPan extends StatefulWidget { 14 | final Function share; 15 | final Function(bool) full; 16 | VideoPlayerPan({ 17 | // this.controlKey, 18 | this.child, 19 | this.share, 20 | this.full, 21 | }); 22 | 23 | // final GlobalKey controlKey; 24 | final Widget child; 25 | 26 | @override 27 | _VideoPlayerPanState createState() => _VideoPlayerPanState(); 28 | } 29 | 30 | class _VideoPlayerPanState extends State 31 | with AfterLayoutMixin { 32 | Offset startPosition; // 起始位置 33 | double movePan; // 偏移量累计总和 34 | double layoutWidth; // 组件宽度 35 | double layoutHeight; // 组件高度 36 | String volumePercentage = ''; // 组件位移描述 37 | double playDialogOpacity = 0.0; 38 | bool allowHorizontal = false; // 是否允许快进 39 | Duration position = Duration(seconds: 0); // 当前时间 40 | double volume = 0.0; 41 | double brightness = 0.0; //亮度 42 | bool brightnessOk = false; // 是否允许调节亮度 43 | 44 | VideoPlayerController get controller => 45 | ControllerWidget.of(context).controller; 46 | bool get videoInit => ControllerWidget.of(context).videoInit; 47 | String get title => ControllerWidget.of(context).title; 48 | 49 | @override 50 | void afterFirstLayout(BuildContext context) { 51 | _reset(context); 52 | Future.delayed(Duration.zero).then((value) async { 53 | FlutterVolume.disableUI(); 54 | brightness = await Screen.brightness; 55 | volume = await FlutterVolume.get(); 56 | setState(() {}); 57 | }); 58 | } 59 | 60 | @override 61 | void dispose() { 62 | super.dispose(); 63 | brightnessOk = false; 64 | allowHorizontal = false; 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return GestureDetector( 70 | onVerticalDragStart: _onVerticalDragStart, 71 | onVerticalDragUpdate: _onVerticalDragUpdate, 72 | onVerticalDragEnd: _onVerticalDragEnd, 73 | onHorizontalDragStart: _onHorizontalDragStart, 74 | onHorizontalDragUpdate: _onHorizontalDragUpdate, 75 | onHorizontalDragEnd: _onHorizontalDragEnd, 76 | child: Container( 77 | child: Stack( 78 | children: [ 79 | widget.child, 80 | Center( 81 | child: AnimatedOpacity( 82 | opacity: playDialogOpacity, 83 | duration: Duration(milliseconds: 500), 84 | child: Container( 85 | padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 6.0), 86 | decoration: BoxDecoration( 87 | color: Colors.black87, 88 | borderRadius: BorderRadius.all(Radius.circular(5.0))), 89 | child: Text( 90 | volumePercentage, 91 | style: TextStyle(color: Colors.white, fontSize: 12), 92 | ), 93 | ), 94 | ), 95 | ), 96 | VideoPlayerControl( 97 | share: widget.share, 98 | full: widget.full, 99 | key: ControllerWidget.of(context).controlKey, 100 | ) 101 | ], 102 | ), 103 | ), 104 | ); 105 | } 106 | 107 | void _onVerticalDragStart(details) async { 108 | _reset(context); 109 | startPosition = details.globalPosition; 110 | if (startPosition.dx < (layoutWidth / 2)) { 111 | /// 左边触摸 112 | brightnessOk = true; 113 | } 114 | } 115 | 116 | double start = 0; 117 | void _onVerticalDragUpdate(details) { 118 | if (!videoInit) { 119 | return; 120 | } 121 | 122 | /// 累计计算偏移量(下滑减少百分比,上滑增加百分比) 123 | movePan += (-details.delta.dy); 124 | if (startPosition.dx < (layoutWidth / 2)) { 125 | /// 左边触摸 126 | if (brightnessOk = true) { 127 | setState(() { 128 | volumePercentage = '亮度:${(_setBrightnessValue() * 100).toInt()}%'; 129 | playDialogOpacity = 1.0; 130 | }); 131 | } 132 | } else { 133 | /// 右边触摸 134 | setState(() { 135 | volumePercentage = '音量:${(_setVerticalValue() * 100).toInt()}%'; 136 | playDialogOpacity = 1.0; 137 | }); 138 | } 139 | onDrag(); 140 | } 141 | 142 | void _onVerticalDragEnd(_) async { 143 | brightness = await Screen.brightness; 144 | volume = await FlutterVolume.get(); 145 | brightnessOk = false; 146 | playDialogOpacity = 0.0; 147 | setState(() {}); 148 | } 149 | 150 | void onDrag() async { 151 | if (!videoInit) { 152 | return; 153 | } 154 | if (startPosition.dx < (layoutWidth / 2)) { 155 | if (brightnessOk) { 156 | await Screen.setBrightness(_setBrightnessValue()); 157 | } 158 | } else { 159 | await FlutterVolume.set(_setVerticalValue()); 160 | } 161 | } 162 | 163 | double _setBrightnessValue() { 164 | // 亮度百分控制 165 | double value = 166 | double.parse((movePan / layoutHeight + brightness).toStringAsFixed(2)); 167 | if (value >= 1.00) { 168 | value = 1.00; 169 | } else if (value <= 0.00) { 170 | value = 0.00; 171 | } 172 | return value; 173 | } 174 | 175 | double _setVerticalValue() { 176 | print(movePan / layoutHeight * 100); 177 | // 声音百分控制 178 | double value = 179 | double.parse((movePan / layoutHeight + volume).toStringAsFixed(2)); 180 | if (value >= 1.0) { 181 | value = 1.0; 182 | } else if (value <= 0.0) { 183 | value = 0.0; 184 | } 185 | return value; 186 | } 187 | 188 | void _reset(BuildContext context) { 189 | startPosition = Offset(0, 0); 190 | movePan = 0; 191 | layoutHeight = context.size.height; 192 | layoutWidth = context.size.width; 193 | volumePercentage = ''; 194 | } 195 | 196 | void _onHorizontalDragStart(DragStartDetails details) async { 197 | _reset(context); 198 | if (!videoInit) { 199 | return; 200 | } 201 | // 获取当前时间 202 | position = controller.value.position; 203 | // 暂停成功后才允许快进手势 204 | allowHorizontal = true; 205 | await controller?.pause(); 206 | } 207 | 208 | void _onHorizontalDragUpdate(DragUpdateDetails details) { 209 | if (!videoInit && !allowHorizontal) { 210 | return; 211 | } 212 | // 累计计算偏移量 213 | movePan += details.delta.dx; 214 | double value = _setHorizontalValue(); 215 | // 用百分比计算出当前的秒数 216 | String currentSecond = 217 | videoTime((value * controller.value.duration?.inMilliseconds).toInt()); 218 | if (value >= 0) { 219 | setState(() { 220 | volumePercentage = '快进至:$currentSecond'; 221 | playDialogOpacity = 1.0; 222 | }); 223 | } else { 224 | setState(() { 225 | volumePercentage = '快退至:${(value * 100).toInt()}%'; 226 | playDialogOpacity = 1.0; 227 | }); 228 | } 229 | } 230 | 231 | void _onHorizontalDragEnd(DragEndDetails details) async { 232 | if (!videoInit && !allowHorizontal) { 233 | return; 234 | } 235 | double value = _setHorizontalValue(); 236 | int current = (value * controller.value.duration?.inMilliseconds).toInt(); 237 | await controller?.seekTo(Duration(milliseconds: current)); 238 | await controller?.play(); 239 | allowHorizontal = false; 240 | setState(() { 241 | playDialogOpacity = 0.0; 242 | }); 243 | } 244 | 245 | double _setHorizontalValue() { 246 | // 进度条百分控制 247 | double valueHorizontal = 248 | double.parse((movePan / layoutWidth).toStringAsFixed(2)); 249 | // 当前进度条百分比 250 | double currentValue = 251 | position.inMilliseconds / controller.value.duration.inMilliseconds; 252 | double value = 253 | double.parse((currentValue + valueHorizontal).toStringAsFixed(2)); 254 | if (value >= 1.00) { 255 | value = 1.00; 256 | } else if (value <= 0.00) { 257 | value = 0.00; 258 | } 259 | return value; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /lib/pages/video/video_player_slider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:feiyu/app/tool/time.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:video_player/video_player.dart'; 5 | 6 | import 'controller_widget.dart'; 7 | 8 | class VideoPlayerSlider extends StatefulWidget { 9 | final Function startPlayControlTimer; 10 | final Timer timer; 11 | 12 | VideoPlayerSlider({this.startPlayControlTimer, this.timer}); 13 | 14 | @override 15 | _VideoPlayerSliderState createState() => _VideoPlayerSliderState(); 16 | } 17 | 18 | class _VideoPlayerSliderState extends State { 19 | VideoPlayerController get controller => 20 | ControllerWidget.of(context).controller; 21 | 22 | bool get videoInit => ControllerWidget.of(context).videoInit; 23 | double progressValue; //进度 24 | String labelProgress; //tip内容 25 | bool handle = false; //判断是否在滑动的标识 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | progressValue = 0.0; 31 | labelProgress = '00:00'; 32 | } 33 | 34 | @override 35 | void didUpdateWidget(VideoPlayerSlider oldWidget) { 36 | super.didUpdateWidget(oldWidget); 37 | var v = controller.value; 38 | if (!handle && videoInit && v != null) { 39 | int position = v.position == null ? 0 : v.position.inMilliseconds; 40 | int duration = v.duration == null ? 0 : v.duration.inMilliseconds; 41 | if (position >= duration) { 42 | position = duration; 43 | } 44 | setState(() { 45 | progressValue = duration == 0 ? 0 : position / duration * 100; 46 | labelProgress = videoTime(position.toInt()); 47 | }); 48 | } 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return SliderTheme( 54 | data: SliderTheme.of(context).copyWith( 55 | //提示进度的气泡文本的颜色 56 | valueIndicatorTextStyle: TextStyle( 57 | color: Colors.black, 58 | ), 59 | ), 60 | child: Slider( 61 | value: progressValue, 62 | label: labelProgress, 63 | divisions: 100, 64 | onChangeStart: _onChangeStart, 65 | onChangeEnd: _onChangeEnd, 66 | onChanged: _onChanged, 67 | min: 0, 68 | max: 100, 69 | activeColor: Colors.yellow, 70 | inactiveColor: Colors.white, 71 | )); 72 | } 73 | 74 | void _onChangeEnd(_) async{ 75 | if (!videoInit) return; 76 | widget.startPlayControlTimer(); //开始计时隐藏控制组件 77 | handle = false; //未在滑动 78 | // 跳转到滑动时间 79 | int duration = controller.value.duration.inMilliseconds; 80 | controller.seekTo( 81 | Duration(milliseconds: (progressValue / 100 * duration).toInt()), 82 | ); 83 | await controller.play();//开始播放 84 | } 85 | 86 | void _onChangeStart(_) async { 87 | if (!videoInit) return; 88 | widget.timer?.cancel(); //停止计时 89 | handle = true; //滑动中 90 | await controller.pause();//暂停 91 | } 92 | 93 | void _onChanged(double value) { 94 | if (!videoInit) return; 95 | widget.timer?.cancel(); //停止计时 96 | int duration = controller.value.duration.inMilliseconds; 97 | setState(() { 98 | //滑动进度 99 | progressValue = value; 100 | //气泡进度 101 | labelProgress = videoTime((value / 100 * duration).toInt()); 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: feiyu 2 | description: A new Flutter project. 3 | 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=2.1.0 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | cupertino_icons: ^0.1.3 13 | dio: ^3.0.9 14 | video_player: ^0.10.11+1 15 | auto_orientation: ^1.0.6 16 | screen: ^0.0.5 17 | flustars: ^0.3.2 18 | dlna: ^0.0.5 19 | fluttertoast: ^4.0.1 20 | xml2json: ^4.2.0 21 | qr_flutter: ^3.2.0 22 | sensors: ^0.4.2+2 23 | 24 | sys_volume: 25 | git: 26 | url: https://github.com/befovy/flutter_volume.git 27 | 28 | 29 | dev_dependencies: 30 | flutter_test: 31 | sdk: flutter 32 | 33 | flutter: 34 | uses-material-design: true 35 | 36 | # To add assets to your application, add an assets section, like this: 37 | # assets: 38 | # - images/a_dot_burr.jpeg 39 | # - images/a_dot_ham.jpeg 40 | 41 | # fonts: 42 | # - family: Schyler 43 | # fonts: 44 | # - asset: fonts/Schyler-Regular.ttf 45 | # - asset: fonts/Schyler-Italic.ttf 46 | # style: italic 47 | # - family: Trajan Pro 48 | # fonts: 49 | # - asset: fonts/TrajanPro.ttf 50 | # - asset: fonts/TrajanPro_Bold.ttf 51 | # weight: 700 52 | # 53 | # For details regarding fonts from package dependencies, 54 | # see https://flutter.dev/custom-fonts/#from-packages 55 | -------------------------------------------------------------------------------- /screenshots/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/1.jpg -------------------------------------------------------------------------------- /screenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/2.jpg -------------------------------------------------------------------------------- /screenshots/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/3.jpg -------------------------------------------------------------------------------- /screenshots/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/4.jpg -------------------------------------------------------------------------------- /screenshots/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/5.jpg -------------------------------------------------------------------------------- /screenshots/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/6.jpg -------------------------------------------------------------------------------- /screenshots/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/7.jpg -------------------------------------------------------------------------------- /screenshots/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/8.jpg -------------------------------------------------------------------------------- /screenshots/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/logo.png --------------------------------------------------------------------------------