├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .gitmodules
├── LICENSE
├── app.json
├── app
├── (tabs)
│ ├── _layout.tsx
│ ├── browse.tsx
│ ├── index.tsx
│ ├── log.tsx
│ ├── manage.tsx
│ └── setting.tsx
├── +html.tsx
├── +not-found.tsx
├── _layout.tsx
└── store
│ ├── index.ts
│ ├── log.ts
│ ├── server.ts
│ └── setting.ts
├── assets
├── fonts
│ └── SpaceMono-Regular.ttf
└── images
│ ├── adaptive-icon.png
│ ├── audio.png
│ ├── directory.png
│ ├── favicon.png
│ ├── file.png
│ ├── icon.png
│ ├── partial-react-logo.png
│ ├── react-logo.png
│ ├── react-logo@2x.png
│ ├── react-logo@3x.png
│ ├── video.png
│ └── 无服务.png
├── babel.config.js
├── components
├── AListWebview.tsx
├── Collapsible.tsx
├── ColorSchemeCard.tsx
├── ColorSchemeText.tsx
├── ExternalLink.tsx
├── HelloWave.tsx
├── NoData.tsx
├── ParallaxScrollView.tsx
├── ThemedText.tsx
├── ThemedView.tsx
├── __tests__
│ ├── ThemedText-test.tsx
│ └── __snapshots__
│ │ └── ThemedText-test.tsx.snap
└── navigation
│ └── TabBarIcon.tsx
├── constants
└── Colors.ts
├── hooks
├── useAppInActive.ts
├── useColorScheme.ts
├── useColorScheme.web.ts
└── useThemeColor.ts
├── images.d.ts
├── ios
├── .gitignore
├── .xcode.env
├── AppDelegate.swift
├── Podfile
├── Podfile.lock
├── Podfile.properties.json
├── PrivacyInfo.xcprivacy
├── alist copy-Info.plist
├── alist.xcodeproj
│ ├── project.pbxproj
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── alist.xcscheme
├── alist.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── alist
│ ├── Images.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Alist-tinted.png
│ │ ├── Alist.jpg
│ │ ├── Alist.png
│ │ └── Contents.json
│ ├── Contents.json
│ ├── SplashScreen.imageset
│ │ ├── Contents.json
│ │ └── alist-splash.png
│ └── SplashScreenBackground.imageset
│ │ ├── Contents.json
│ │ └── background-black.jpg
│ ├── Info.plist
│ ├── SplashScreen.storyboard
│ ├── Supporting
│ └── Expo.plist
│ ├── alist-Bridging-Header.h
│ ├── alist.entitlements
│ └── noop-file.swift
├── native
├── Alistlib
│ ├── Alist.m
│ └── Alist.swift
├── AppInfo
│ ├── AppInfo.m
│ └── AppInfo.swift
├── BGRunManager
│ ├── HCKeepBGRunManager.m
│ ├── HCKeepBGRunManager.swift
│ └── jm.mp3
├── CloudKitManager
│ ├── CloudKitManager.m
│ └── CloudKitManager.swift
├── NotificationManager
│ ├── NotificationManager.m
│ └── NotificationManager.swift
└── VideoPlayer
│ ├── AudioPlayer.m
│ ├── AudioPlayer.swift
│ ├── AudioView.swift
│ ├── IOSView.swift
│ ├── PlayerViewInstance.swift
│ ├── VideoPlayer.m
│ ├── VideoPlayer.swift
│ ├── VideoView.swift
│ └── audio.png
├── package.json
├── scripts
└── reset-project.js
├── tsconfig.json
└── yarn.lock
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: "Build iOS app"
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | release_name:
7 | description: 'Enter the name of the release'
8 | required: false
9 | alist_version:
10 | description: 'Enter the version of the AList'
11 | required: true
12 |
13 | permissions:
14 | contents: write
15 |
16 | jobs:
17 | build:
18 | runs-on: macos-15
19 | steps:
20 | - name: check Xcode version
21 | run: /usr/bin/xcodebuild -version
22 | - uses: pnpm/action-setup@v4
23 | with:
24 | version: 9.9.0
25 | - name: checkout repository
26 | uses: actions/checkout@v3
27 | with:
28 | submodules: 'recursive'
29 | - uses: actions/setup-go@v5
30 | with:
31 | go-version-file: 'alist/go.mod'
32 |
33 | - name: build alist-web
34 | run: cd alist-web && pnpm i && pnpm run build && mv ./dist ../alist/public/
35 |
36 | - name: build alist
37 | run: |
38 | cd alist && \
39 | go install golang.org/x/mobile/cmd/gomobile@latest && \
40 | gomobile init && \
41 | gomobile bind -target ios -bundleid com.gendago.alist -o ../ios/alist/Alistlib.xcframework -ldflags "-s -w -X 'github.com/alist-org/alist/v3/internal/conf.Version=${{ github.event.inputs.alist_version }}'" github.com/alist-org/alist/v3/alistlib
42 |
43 | - name: modify Podfile
44 | run: sed -i '' 's/alist/alist-without-icloud/g' ios/Podfile
45 |
46 | - name: install dependencies
47 | run: yarn && npx pod-install
48 |
49 | - name: build archive
50 | run: |
51 | xcodebuild -workspace ./ios/alist.xcworkspace \
52 | -scheme alist-without-icloud \
53 | -configuration Release clean archive \
54 | -archivePath unsigned.xcarchive \
55 | CODE_SIGN_IDENTITY="" \
56 | CODE_SIGNING_REQUIRED=NO \
57 | CODE_SIGNING_ALLOWED=NO
58 |
59 | - name: export ipa
60 | env:
61 | EXPORT_OPTIONS_PLIST: ${{ secrets.EXPORT_OPTIONS_PLIST }}
62 | run: |
63 | cp -r ./unsigned.xcarchive/Products/Applications ./Payload && \
64 | zip -r AListServer.ipa ./Payload/
65 |
66 | - name: Upload to Release
67 | if: ${{ github.event.inputs.release_name }}
68 | uses: "marvinpinto/action-automatic-releases@latest"
69 | with:
70 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
71 | automatic_release_tag: ${{ github.event.inputs.release_name }}
72 | prerelease: false
73 | files: AListServer.ipa
74 |
75 | - name: Upload to Artifacts
76 | if: ${{ !github.event.inputs.release_name }}
77 | uses: actions/upload-artifact@v4
78 | with:
79 | name: AListServer
80 | path: AListServer.ipa
81 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
16 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
17 | # The following patterns were generated by expo-cli
18 |
19 | expo-env.d.ts
20 | # @end expo-cli
21 | .idea/
22 | ios/alist/Alistlib.xcframework/
23 | unsigned.xcarchive
24 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "alist"]
2 | path = alist
3 | url = https://github.com/gendago/alist-ios.git
4 | [submodule "alist-web"]
5 | path = alist-web
6 | url = https://github.com/gendago/alist-web.git
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "alist",
4 | "slug": "alist",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "splash": {
11 | "image": "./assets/images/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "ios": {
16 | "supportsTablet": true,
17 | "bundleIdentifier": "com.gendago.alist"
18 | },
19 | "android": {
20 | "adaptiveIcon": {
21 | "foregroundImage": "./assets/images/adaptive-icon.png",
22 | "backgroundColor": "#ffffff"
23 | }
24 | },
25 | "web": {
26 | "bundler": "metro",
27 | "output": "static",
28 | "favicon": "./assets/images/favicon.png"
29 | },
30 | "plugins": [
31 | "expo-router"
32 | ],
33 | "experiments": {
34 | "typedRoutes": true
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import {Tabs, useFocusEffect} from 'expo-router';
2 | import React, {useCallback, useEffect} from 'react';
3 |
4 | import { TabBarIcon } from '@/components/navigation/TabBarIcon';
5 | import { Colors } from '@/constants/Colors';
6 | import {appendLog} from "@/app/store/log";
7 | import {NativeEventEmitter, NativeModules, useColorScheme} from "react-native";
8 | import {useAppDispatch, useAppSelector} from "@/app/store";
9 | import {refreshIsRunning} from "@/app/store/server";
10 | import useAppInActive from "@/hooks/useAppInActive";
11 | import axios from "axios";
12 |
13 | const {Alist, HCKeepBGRunManager} = NativeModules;
14 | const eventEmitter = new NativeEventEmitter(Alist);
15 |
16 | export default function TabLayout() {
17 | const colorScheme = useColorScheme();
18 | const dispatch = useAppDispatch();
19 | const backgroundMode = useAppSelector(state => state.setting.backgroundMode)
20 | const autoStopHours = useAppSelector(state => state.setting.autoStopHours) || 0
21 | const isRunning = useAppSelector(state => state.server.isRunning)
22 | const appInActive = useAppInActive()
23 |
24 | const checkIsRunning = useCallback(async () => {
25 | try {
26 | await axios.get('http://127.0.0.1:5244/ping', {
27 | timeout: 1000
28 | })
29 | console.log('检查服务:可用')
30 | } catch (e) {
31 | console.log('检查服务:不可用')
32 | // 如果服务实际上不可用,则自动关闭,更新状态
33 | await Alist.stop()
34 | dispatch(refreshIsRunning())
35 | }
36 | }, [])
37 |
38 | useEffect(() => {
39 | const onLog = eventEmitter.addListener('onLog', (logInfo) => {
40 | dispatch(appendLog(logInfo))
41 | });
42 | const onProcessExit = eventEmitter.addListener('onProcessExit', (logInfo) => {
43 | console.log('onProcessExit', logInfo)
44 | dispatch(refreshIsRunning())
45 | });
46 | const onShutdown = eventEmitter.addListener('onShutdown', (logInfo) => {
47 | console.log('onShutdown', logInfo)
48 | dispatch(refreshIsRunning())
49 | HCKeepBGRunManager.stopBGRun()
50 | });
51 | const onStartError = eventEmitter.addListener('onStartError', (logInfo) => {
52 | console.log('onStartError', logInfo)
53 | dispatch(refreshIsRunning())
54 | });
55 |
56 | return () => {
57 | onLog.remove();
58 | onProcessExit.remove();
59 | onShutdown.remove();
60 | onStartError.remove();
61 | }
62 | }, [])
63 |
64 | useFocusEffect(useCallback(() => {
65 | dispatch(refreshIsRunning())
66 | }, [appInActive]));
67 |
68 | useEffect(() => {
69 | if (backgroundMode && isRunning) {
70 | if (appInActive) {
71 | HCKeepBGRunManager.stopBGRun()
72 | } else {
73 | HCKeepBGRunManager.startBGRun()
74 | }
75 | }
76 | }, [appInActive, backgroundMode, isRunning])
77 |
78 | useEffect(() => {
79 | if (appInActive && isRunning) {
80 | // 切到前台时,如果服务处于运行状态,通过接口再检测一下服务是否可用,防止服务进程被系统杀掉
81 | checkIsRunning()
82 | }
83 | }, [appInActive, isRunning, checkIsRunning]);
84 |
85 | useEffect(() => {
86 | Alist.setAutoStopHours(autoStopHours)
87 | }, []);
88 |
89 | return (
90 |
101 | (
107 |
108 | ),
109 | }}
110 | />
111 | (
116 |
117 | ),
118 | }}
119 | />
120 | (
125 |
126 | ),
127 | }}
128 | />
129 | (
134 |
135 | ),
136 | }}
137 | />
138 | (
143 |
144 | ),
145 | }}
146 | />
147 |
148 | );
149 | }
150 |
--------------------------------------------------------------------------------
/app/(tabs)/browse.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import AListWebview from "@/components/AListWebview";
3 |
4 | export default function Browse() {
5 |
6 | return (
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/app/(tabs)/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Image,
3 | StyleSheet,
4 | Platform,
5 | NativeModules,
6 | Button,
7 | View,
8 | Switch,
9 | NativeEventEmitter,
10 | ScrollView, TouchableOpacity
11 | } from 'react-native';
12 | import React, {useCallback, useEffect, useState} from "react";
13 | import {useFocusEffect} from "expo-router";
14 | import { addEventListener } from "@react-native-community/netinfo";
15 | import {useAppDispatch, useAppSelector} from "@/app/store";
16 | import {refreshIsRunning} from "@/app/store/server";
17 | import Clipboard from '@react-native-clipboard/clipboard';
18 | import Toast from "react-native-root-toast";
19 | import Text from '@/components/ColorSchemeText'
20 | import ColorSchemeCard from "@/components/ColorSchemeCard";
21 | import axios from "axios/index";
22 | import RNFS from "react-native-fs";
23 |
24 | const {Alist, NotificationManager, AppInfo} = NativeModules;
25 | const DEFAULT_PASSWORD = 'admin'
26 |
27 | export default function HomeScreen() {
28 | const isRunning = useAppSelector(state => state.server.isRunning)
29 | const autoRun = useAppSelector(state => state.setting.autoRun)
30 | const iCloudSync = useAppSelector(state => state.setting.iCloudSync)
31 | const dispatch = useAppDispatch()
32 | const [adminPwd, setAdminPwd] = useState('')
33 | const [adminUsername, setAdminUsername] = useState('')
34 | const [ip, setIP] = useState(null)
35 | const [loading, setLoading] = useState(false)
36 | const start = async () => {
37 | if (isRunning) return
38 | setLoading(true)
39 | try {
40 | await Alist.start();
41 | // 服务启动需要时间,这里做一个延时检测
42 | await new Promise(resolve => setTimeout(resolve, 500))
43 | await axios.get('http://127.0.0.1:5244/ping', {
44 | timeout: 1000
45 | })
46 | await dispatch(refreshIsRunning())
47 | } catch (e) {
48 | console.error(e);
49 | }
50 | setLoading(false)
51 | };
52 |
53 | const stop = async () => {
54 | if (!isRunning) return
55 | try {
56 | await Alist.stop()
57 | dispatch(refreshIsRunning())
58 | } catch (e) {
59 | console.error(e);
60 | }
61 | };
62 | const updateAdminInfo = useCallback(async () => {
63 | const pwd = await Alist.getAdminPassword()
64 | const username = await Alist.getAdminUsername()
65 | if (!pwd) {
66 | // 只有首次启动服务会获取不到密码,那么直接设置初始密码为admin
67 | await changePassword(DEFAULT_PASSWORD)
68 | setAdminPwd(DEFAULT_PASSWORD)
69 | } else {
70 | setAdminPwd(pwd)
71 | }
72 | setAdminUsername(username)
73 | }, [setAdminPwd, setAdminUsername])
74 |
75 | const toggleSwitch = useCallback(() => {
76 | if (isRunning) {
77 | stop()
78 | } else {
79 | start()
80 | }
81 | }, [isRunning, stop, start])
82 |
83 | const changePassword = useCallback((pwd: string) => {
84 | return Alist.setAdminPassword(pwd)
85 | }, [])
86 |
87 | const copy = useCallback((ip: string) => {
88 | Clipboard.setString(ip);
89 | Toast.show('已复制到剪切板', {
90 | position: Toast.positions.CENTER
91 | })
92 | }, [])
93 |
94 | const init = useCallback( async () => {
95 | try {
96 | await Alist.init()
97 | try {
98 | // iCloud同步 失败不阻塞主功能
99 | if (iCloudSync) {
100 | await Alist.iCloudRestore()
101 | }
102 | } catch (e) {
103 | console.error(e)
104 | }
105 | if (autoRun) {
106 | const isRunning = await Alist.isRunning()
107 | if (!isRunning) {
108 | // 自动启动
109 | start()
110 | NotificationManager.scheduleNotification("AListServer", "服务正在运行中")
111 | }
112 | }
113 | } catch (e: any) {
114 | console.error(e)
115 | Toast.show(e?.message ?? "AList初始化失败", {
116 | position: Toast.positions.CENTER
117 | })
118 | }
119 | }, [autoRun, start, iCloudSync])
120 |
121 | useFocusEffect(React.useCallback(() => {
122 | if (isRunning) {
123 | updateAdminInfo()
124 | }
125 | }, [isRunning, updateAdminInfo]));
126 |
127 | useEffect(() => {
128 | return addEventListener((state) => {
129 | AppInfo.getWiFiAddress()
130 | .then((address: string) => {
131 | if (state.type === 'wifi') {
132 | setIP(address)
133 | } else {
134 | setIP(null)
135 | }
136 | })
137 | .catch((e: any) => {
138 | console.error(e)
139 | if (state.type === 'wifi') {
140 | setIP('0.0.0.0')
141 | } else {
142 | setIP(null)
143 | }
144 | })
145 | });
146 | }, []);
147 |
148 | useEffect(() => {
149 | init()
150 | }, []);
151 |
152 | return (
153 |
154 |
155 |
156 |
157 | 服务状态:{loading ? '启动中...' : isRunning ? '运行中' : '未运行'}
158 |
165 |
166 |
167 |
168 |
169 | 账号信息
170 |
171 |
172 | 用户名
173 | {isRunning ? adminUsername : '请先启动服务'}
174 |
175 |
176 | 密码
177 | {isRunning ? adminPwd : '请先启动服务'}
178 |
179 |
180 |
181 |
182 | WebDAV信息
183 |
184 |
185 | 服务器地址
186 |
187 | {ip ? (
188 | copy(ip)}>
189 | {ip}(局域网访问)
190 |
191 | ) : null }
192 | copy('127.0.0.1')}>
193 | 127.0.0.1(限本机访问)
194 |
195 |
196 |
197 |
198 | 端口
199 | copy('5244')}>
200 | 5244
201 |
202 |
203 |
204 | 路径
205 | copy('dav')}>
206 | dav
207 |
208 |
209 |
210 | 用户名/密码
211 | 同“账号信息”
212 |
213 |
214 | 请保持App前台运行,否则服务可能不可用
215 |
216 |
217 | );
218 | }
219 |
220 | const styles = StyleSheet.create({
221 | titleContainer: {
222 | flexDirection: 'row',
223 | alignItems: 'center',
224 | gap: 8,
225 | },
226 | stepContainer: {
227 | gap: 8,
228 | marginBottom: 8,
229 | },
230 | reactLogo: {
231 | height: 178,
232 | width: 290,
233 | bottom: 0,
234 | left: 0,
235 | position: 'absolute',
236 | },
237 | container: {
238 | paddingTop: 16,
239 | paddingBottom: 32,
240 | flex: 1,
241 | },
242 | cardItem: {
243 | display: 'flex',
244 | flexDirection: 'row',
245 | justifyContent: 'space-between',
246 | alignItems: 'center',
247 | height: 50,
248 | },
249 | multiRow: {
250 | minHeight: 50,
251 | alignItems: 'flex-start',
252 | },
253 | cardItemBorderBottom: {
254 | borderBottomWidth: 1,
255 | borderBottomColor: 'rgb(228, 228, 228)',
256 | },
257 | cardMarginTop: {
258 | marginTop: 40,
259 | },
260 | bold: {
261 | fontWeight: 'bold',
262 | },
263 | runningTip: {
264 | color: 'gray',
265 | textAlign: 'center',
266 | marginTop: 36,
267 | }
268 | });
269 |
--------------------------------------------------------------------------------
/app/(tabs)/log.tsx:
--------------------------------------------------------------------------------
1 | import React, {useLayoutEffect} from 'react'
2 | import {FlatList, StyleSheet, useColorScheme, View} from "react-native";
3 | import dayjs from 'dayjs'
4 | import Ionicons from "@expo/vector-icons/Ionicons";
5 | import {useNavigation} from "expo-router";
6 | import {clearLogs, LogLevel} from "@/app/store/log";
7 | import {useAppDispatch, useAppSelector} from "@/app/store";
8 | import NoData from "@/components/NoData";
9 | import {Colors} from "@/constants/Colors";
10 | import Text from '@/components/ColorSchemeText'
11 |
12 |
13 | const getLevelText = (level: LogLevel) => {
14 | return {
15 | [LogLevel.error]: 'error',
16 | [LogLevel.warn]: 'warn',
17 | [LogLevel.info]: 'info',
18 | [LogLevel.debug]: 'debug'
19 | }[level] as 'error'|'warn'|'info'|'debug'
20 | }
21 | export default function Log() {
22 | const logs = useAppSelector(
23 | (state) => state.log.logs,
24 | );
25 | const navigation = useNavigation()
26 | const dispatch = useAppDispatch()
27 | const colorScheme = useColorScheme()
28 |
29 | useLayoutEffect(() => {
30 | navigation.setOptions({
31 | headerRight: () => logs.length ? (
32 | dispatch(clearLogs())}
37 | />
38 | ) : null
39 | });
40 | }, [logs]);
41 |
42 | return logs.length ? (
43 |
44 | {
47 | const levelText = getLevelText(item.level) ?? 'unknow'
48 | const color: string = {
49 | error: 'red',
50 | warn: '#faad14',
51 | info: Colors[colorScheme ?? 'light'].text,
52 | debug: 'gray',
53 | unknow: '#faad14',
54 | }[levelText]
55 | return (
56 |
57 | {levelText}
58 |
59 | {item.message}
60 | {dayjs(item.time).format('MM-DD HH:mm:ss')}
61 |
62 |
63 | )
64 | }}
65 | />
66 |
67 | ) : (
68 |
69 | )
70 | }
71 |
72 | const styles:Record = StyleSheet.create({
73 | container: {
74 | flex: 1,
75 | paddingHorizontal: 12,
76 | paddingVertical: 12,
77 | },
78 | item : {
79 | flex: 1,
80 | width: '100%',
81 | display: 'flex',
82 | flexDirection: 'row',
83 | alignItems: 'center',
84 | justifyContent: 'center',
85 | marginBottom: 12,
86 | },
87 | level: {
88 | flexBasis: 42,
89 | flexShrink: 0,
90 | },
91 | message: {
92 | fontSize: 16,
93 | marginBottom: 4,
94 | },
95 | })
96 |
--------------------------------------------------------------------------------
/app/(tabs)/manage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import AListWebview from "@/components/AListWebview";
3 |
4 | export default function Manage() {
5 |
6 | return (
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/app/(tabs)/setting.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | StyleSheet,
3 | View,
4 | Switch,
5 | TouchableOpacity,
6 | Modal,
7 | Linking, TouchableWithoutFeedback, NativeModules, useColorScheme, ActivityIndicator, ScrollView, Alert
8 | } from 'react-native';
9 | import Ionicons from "@expo/vector-icons/Ionicons";
10 | import React, {useCallback, useEffect, useState} from "react";
11 | import {FontAwesome} from "@expo/vector-icons";
12 | import {useAppDispatch, useAppSelector} from "@/app/store";
13 | import {setAutoRun, setAutoStopHours, setBackgroundMode, setICloudSync} from "@/app/store/setting";
14 | import ColorSchemeCard from "@/components/ColorSchemeCard";
15 | import Text from '@/components/ColorSchemeText'
16 | import {Colors} from "@/constants/Colors";
17 | import Toast from "react-native-root-toast";
18 |
19 | const { AppInfo, NotificationManager, Alist } = NativeModules;
20 |
21 | export default function Setting() {
22 | const [modalVisible, setModalVisible] = useState(false);
23 | const [version, setVersion] = useState('1.0')
24 | const [alistVersion, setAlistVersion] = useState('dev')
25 | const backgroundMode = useAppSelector(state => state.setting.backgroundMode)
26 | const autoRun = useAppSelector(state => state.setting.autoRun)
27 | const iCloudSync = useAppSelector(state => state.setting.iCloudSync)
28 | const autoStopHours = useAppSelector(state => state.setting.autoStopHours) || 0
29 | const dispatch = useAppDispatch()
30 | const colorScheme = useColorScheme()
31 | const [iCloudBackupLoading, setCloudBackupLoading] = useState(false)
32 | const [userRecordId, setUserRecordId] = useState(null)
33 |
34 | const showAbout = useCallback(() => {
35 | setModalVisible(true)
36 | }, [setModalVisible])
37 |
38 | const getAppVersion = useCallback(async () => {
39 | try {
40 | const version = await AppInfo.getAppVersion();
41 | setVersion(version)
42 | } catch (error) {
43 | console.error(error);
44 | }
45 | }, []);
46 |
47 | const getAlistVersion = useCallback(async () => {
48 | try {
49 | const version = await Alist.getVersion();
50 | setAlistVersion(version)
51 | } catch (error) {
52 | console.error(error);
53 | }
54 | }, []);
55 |
56 | const iCloudSwitchChange = useCallback(async (value: boolean) => {
57 | if (value) {
58 | setCloudBackupLoading(true)
59 | try {
60 | try {
61 | // 先尝试恢复
62 | await Alist.iCloudRestore()
63 | Toast.show("已将iCloud数据同步至本地", {
64 | position: Toast.positions.CENTER
65 | })
66 | } catch (e) {
67 | // 恢复失败再尝试备份
68 | console.error(e)
69 | await Alist.iCloudBackup()
70 | Toast.show("已将本地数据同步至iCloud", {
71 | position: Toast.positions.CENTER
72 | })
73 | }
74 | dispatch(setICloudSync(true))
75 | } catch (e: any) {
76 | Toast.show(e?.message ?? "iCloud同步开启失败", {
77 | position: Toast.positions.CENTER
78 | })
79 | }
80 | setCloudBackupLoading(false)
81 | } else {
82 | dispatch(setICloudSync(false))
83 | }
84 | }, [setCloudBackupLoading, dispatch])
85 |
86 | useEffect(() => {
87 | getAppVersion()
88 | }, [getAppVersion]);
89 |
90 | useEffect(() => {
91 | getAlistVersion()
92 | }, [getAlistVersion]);
93 |
94 | useEffect(() => {
95 | NativeModules?.CloudKitManager?.getUserRecordID?.()
96 | .then(setUserRecordId)
97 | }, []);
98 |
99 | return (
100 |
101 |
102 | 通用
103 |
104 |
105 |
106 | 后台运行
107 | 开启后服务常驻后台,息屏也可访问服务
108 |
109 | {
113 | dispatch(setBackgroundMode(value))
114 | }}
115 | value={backgroundMode}
116 | />
117 |
118 | {backgroundMode && (
119 |
120 |
121 | 自动停止
122 | X小时未使用时自动停止服务
123 |
124 | {
125 | Alert.prompt(
126 | '自动停止时间',
127 | '建议设置为4\n因为单集影视通常不超过4小时\n如设置为0代表不自动停止',
128 | (text) => {
129 | if (/^\d+$/.test(text)) {
130 | dispatch(setAutoStopHours(Number(text)))
131 | } else {
132 | Alert.alert('请输入数字', '代表X小时未使用时自动停止服务', [
133 | {
134 | text: '确定',
135 | style: 'cancel',
136 | },
137 | ]);
138 | }
139 | },
140 | 'plain-text',
141 | '4',
142 | 'number-pad'
143 | );
144 | }}>
145 |
146 | {autoStopHours === 0 ? '不停止' : `${autoStopHours}小时`}
147 |
153 |
154 |
155 |
156 | )}
157 |
158 |
159 | 自动运行
160 | App冷启动时自动启动服务
161 |
162 | {
166 | if (value) {
167 | NotificationManager.requestAuthorization()
168 | }
169 | dispatch(setAutoRun(value))
170 | }}
171 | value={autoRun}
172 | />
173 |
174 | {userRecordId ? (
175 |
176 |
177 |
178 | iCloud同步
179 | {iCloudSync && iCloudSwitchChange(true)} color={'#D1D1D6'}/>}
180 |
181 | {userRecordId}
182 |
183 | {userRecordId ? iCloudBackupLoading ? : (
184 |
190 | ) : null}
191 |
192 | ) : null}
193 |
194 | 版本信息
195 |
196 |
197 | App版本
198 | {version}
199 |
200 |
201 | AList版本
202 | {alistVersion}
203 |
204 |
205 | 关于
206 |
207 |
208 |
209 | 关于
210 |
216 |
217 |
218 | Linking.openURL('https://alist-server.notion.site/AListServer-3dc08df0909f45a3a54c3624119ffaed')}>
219 |
220 | 常见问题
221 |
227 |
228 |
229 | Linking.openURL('https://t.me/+3BR_rxBF8LxmNGY1')}>
230 |
231 | 加入交流群
232 |
233 |
234 |
235 |
236 |
237 |
242 | setModalVisible(!modalVisible)}>
243 |
244 |
245 | 本应用遵循AGPL3.0开源协议
246 | Linking.openURL('https://github.com/gendago/alist-expo')}>
247 | alist-expo
248 |
249 | Linking.openURL('https://github.com/gendago/alist-ios')}>
250 | alist-ios
251 |
252 |
253 |
254 |
255 |
256 |
257 | );
258 | }
259 |
260 | const styles = StyleSheet.create({
261 | titleContainer: {
262 | flexDirection: 'row',
263 | alignItems: 'center',
264 | gap: 8,
265 | },
266 | stepContainer: {
267 | gap: 8,
268 | marginBottom: 8,
269 | },
270 | reactLogo: {
271 | height: 178,
272 | width: 290,
273 | bottom: 0,
274 | left: 0,
275 | position: 'absolute',
276 | },
277 | container: {
278 | paddingHorizontal: 16,
279 | paddingVertical: 16,
280 | flex: 1,
281 | },
282 | cardTitle: {
283 | color: 'gray',
284 | fontSize: 14,
285 | textAlign: 'left',
286 | marginBottom: 12,
287 | paddingLeft: 14,
288 | },
289 | cardItem: {
290 | display: 'flex',
291 | flexDirection: 'row',
292 | justifyContent: 'space-between',
293 | alignItems: 'center',
294 | height: 50,
295 | },
296 | cardItemLarge: {
297 | height: 65,
298 | },
299 | cardItemBorderBottom: {
300 | borderBottomWidth: 1,
301 | borderBottomColor: 'rgb(228, 228, 228)',
302 | },
303 | cardMarginTop: {
304 | marginTop: 24,
305 | },
306 | bold: {
307 | fontWeight: 'bold',
308 | },
309 | centeredView: {
310 | flex: 1,
311 | justifyContent: 'center',
312 | alignItems: 'center',
313 | backgroundColor: 'rgba(0, 0, 0, 0.8)'
314 | },
315 | modalView: {
316 | margin: 20,
317 | backgroundColor: 'white',
318 | borderRadius: 20,
319 | padding: 50,
320 | alignItems: 'center',
321 | shadowColor: '#000',
322 | shadowOffset: {
323 | width: 0,
324 | height: 2,
325 | },
326 | shadowOpacity: 0.25,
327 | shadowRadius: 4,
328 | elevation: 5,
329 | },
330 | button: {
331 | borderRadius: 20,
332 | padding: 10,
333 | elevation: 2,
334 | },
335 | buttonOpen: {
336 | backgroundColor: '#F194FF',
337 | },
338 | buttonClose: {
339 | backgroundColor: '#2196F3',
340 | },
341 | textStyle: {
342 | color: 'white',
343 | fontWeight: 'bold',
344 | textAlign: 'center',
345 | },
346 | modalText: {
347 | marginBottom: 15,
348 | textAlign: 'center',
349 | },
350 | itemTitle: {
351 | fontSize: 15,
352 | },
353 | itemDescription: {
354 | fontSize: 12,
355 | color: 'gray',
356 | marginTop: 4,
357 | }
358 | });
359 |
--------------------------------------------------------------------------------
/app/+html.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollViewStyleReset } from 'expo-router/html';
2 | import { type PropsWithChildren } from 'react';
3 |
4 | /**
5 | * This file is web-only and used to configure the root HTML for every web page during static rendering.
6 | * The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs.
7 | */
8 | export default function Root({ children }: PropsWithChildren) {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 | {/*
17 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
18 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
19 | */}
20 |
21 |
22 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
23 |
24 | {/* Add any additional elements that you want globally available on web... */}
25 |
26 | {children}
27 |
28 | );
29 | }
30 |
31 | const responsiveBackground = `
32 | body {
33 | background-color: #fff;
34 | }
35 | @media (prefers-color-scheme: dark) {
36 | body {
37 | background-color: #000;
38 | }
39 | }`;
40 |
--------------------------------------------------------------------------------
/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Link, Stack } from 'expo-router';
2 | import { StyleSheet } from 'react-native';
3 |
4 | import { ThemedText } from '@/components/ThemedText';
5 | import { ThemedView } from '@/components/ThemedView';
6 |
7 | export default function NotFoundScreen() {
8 | return (
9 | <>
10 |
11 |
12 | This screen doesn't exist.
13 |
14 | Go to home screen!
15 |
16 |
17 | >
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | alignItems: 'center',
25 | justifyContent: 'center',
26 | padding: 20,
27 | },
28 | link: {
29 | marginTop: 15,
30 | paddingVertical: 15,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
2 | import { useFonts } from 'expo-font';
3 | import { Stack } from 'expo-router';
4 | import * as SplashScreen from 'expo-splash-screen';
5 | import { useEffect } from 'react';
6 | import 'react-native-reanimated';
7 | import { RootSiblingParent } from 'react-native-root-siblings';
8 |
9 | import { useColorScheme } from '@/hooks/useColorScheme';
10 | import {Provider} from "react-redux";
11 | import store from "@/app/store";
12 | import {LogBox} from "react-native";
13 | import {persistStore} from "redux-persist";
14 | import {PersistGate} from "redux-persist/integration/react";
15 |
16 | // Prevent the splash screen from auto-hiding before asset loading is complete.
17 | SplashScreen.preventAutoHideAsync();
18 | LogBox.ignoreAllLogs(true);
19 |
20 | const persistor = persistStore(store);
21 |
22 | export default function RootLayout() {
23 | const colorScheme = useColorScheme();
24 | const [loaded] = useFonts({
25 | SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
26 | });
27 |
28 | useEffect(() => {
29 | if (loaded) {
30 | SplashScreen.hideAsync();
31 | }
32 | }, [loaded]);
33 |
34 | if (!loaded) {
35 | return null;
36 | }
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/app/store/index.ts:
--------------------------------------------------------------------------------
1 | import {combineReducers, configureStore} from '@reduxjs/toolkit';
2 | import log from './log';
3 | import server from './server';
4 | import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux";
5 | import {
6 | persistReducer,
7 | FLUSH,
8 | REHYDRATE,
9 | PAUSE,
10 | PERSIST,
11 | PURGE,
12 | REGISTER,
13 | } from 'redux-persist';
14 | import AsyncStorage from '@react-native-async-storage/async-storage';
15 | import setting from "@/app/store/setting";
16 |
17 | const persistConfig = {
18 | key: 'alist',
19 | storage: AsyncStorage,
20 | whitelist: ['setting'],
21 | };
22 |
23 | const reducers = combineReducers({
24 | log,
25 | server,
26 | setting,
27 | });
28 |
29 | const persistedReducer = persistReducer(persistConfig, reducers);
30 |
31 | const store= configureStore({
32 | reducer: persistedReducer,
33 | middleware: getDefaultMiddleware =>
34 | getDefaultMiddleware({
35 | serializableCheck: {
36 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
37 | },
38 | }),
39 | });
40 |
41 | export type RootState = ReturnType
42 | export type AppDispatch = typeof store.dispatch
43 |
44 | export const useAppSelector: TypedUseSelectorHook = useSelector
45 | export const useAppDispatch = useDispatch.withTypes()
46 |
47 | export default store
48 |
--------------------------------------------------------------------------------
/app/store/log.ts:
--------------------------------------------------------------------------------
1 | import {createSlice, PayloadAction} from '@reduxjs/toolkit';
2 |
3 | export enum LogLevel {
4 | error = 2,
5 | warn = 3,
6 | info = 4,
7 | debug = 5,
8 | }
9 | export interface LogItem {
10 | level: LogLevel;
11 | message: string;
12 | time: number;
13 | }
14 |
15 | type SliceState = { logs: LogItem[] }
16 |
17 | export const logSlice = createSlice({
18 | name: 'log',
19 | initialState: {
20 | logs: [],
21 | } satisfies SliceState as SliceState,
22 | reducers: {
23 | appendLog(state, {payload}) {
24 | state.logs = [payload, ...state.logs];
25 | },
26 | clearLogs(state) {
27 | state.logs = [];
28 | },
29 | },
30 | });
31 |
32 | export const {
33 | appendLog,
34 | clearLogs,
35 | } = logSlice.actions;
36 |
37 | export default logSlice.reducer;
38 |
--------------------------------------------------------------------------------
/app/store/server.ts:
--------------------------------------------------------------------------------
1 | import {createSlice, Dispatch} from '@reduxjs/toolkit';
2 | import {NativeModules} from "react-native";
3 |
4 | const {Alist} = NativeModules;
5 |
6 | type SliceState = { isRunning: boolean; }
7 |
8 | export const serverSlice = createSlice({
9 | name: 'server',
10 | initialState: {
11 | isRunning: false,
12 | } satisfies SliceState as SliceState,
13 | reducers: {
14 | setIsRunning(state, {payload}) {
15 | state.isRunning = payload;
16 | },
17 | },
18 | });
19 |
20 | export const {
21 | setIsRunning,
22 | } = serverSlice.actions;
23 |
24 | export function refreshIsRunning() {
25 | return async (dispatch: Dispatch) => {
26 | const isRunning = await Alist.isRunning()
27 | dispatch(setIsRunning(isRunning))
28 | }
29 | }
30 |
31 | export default serverSlice.reducer;
32 |
--------------------------------------------------------------------------------
/app/store/setting.ts:
--------------------------------------------------------------------------------
1 | import {createSlice} from '@reduxjs/toolkit';
2 | import {NativeModules} from "react-native";
3 |
4 | const {Alist} = NativeModules;
5 |
6 | type SettingState = { backgroundMode: boolean; autoRun: boolean; iCloudSync: boolean; autoStopHours: number;}
7 |
8 | export const settingSlice = createSlice({
9 | name: 'setting',
10 | initialState: {
11 | backgroundMode: false,
12 | autoRun: false,
13 | iCloudSync: false,
14 | autoStopHours: 0,
15 | } satisfies SettingState as SettingState,
16 | reducers: {
17 | setBackgroundMode(state, {payload}) {
18 | state.backgroundMode = payload;
19 | },
20 | setAutoRun(state, {payload}) {
21 | state.autoRun = payload;
22 | },
23 | setICloudSync(state, {payload}) {
24 | state.iCloudSync = payload;
25 | Alist.iCloudSwitch(payload)
26 | },
27 | setAutoStopHours(state, {payload}) {
28 | state.autoStopHours = payload;
29 | Alist.setAutoStopHours(payload)
30 | },
31 | },
32 | });
33 |
34 | export const {
35 | setBackgroundMode,
36 | setAutoRun,
37 | setICloudSync,
38 | setAutoStopHours,
39 | } = settingSlice.actions;
40 |
41 | export default settingSlice.reducer;
42 |
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/audio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/audio.png
--------------------------------------------------------------------------------
/assets/images/directory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/directory.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/file.png
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/assets/images/video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/video.png
--------------------------------------------------------------------------------
/assets/images/无服务.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/assets/images/无服务.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/components/AListWebview.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | ForwardedRef,
3 | forwardRef,
4 | useCallback,
5 | useEffect, useImperativeHandle, useLayoutEffect, useRef,
6 | useState
7 | } from 'react'
8 | import {WebView} from 'react-native-webview';
9 | import {NativeModules, View, Linking, ActivityIndicator} from "react-native";
10 | import {useFocusEffect, useNavigation} from "expo-router";
11 | import {useAppSelector} from "@/app/store";
12 | import axios from "axios";
13 | import sha256 from 'sha256'
14 | import Toast from "react-native-root-toast";
15 | import Ionicons from "@expo/vector-icons/Ionicons";
16 | import {Feather} from "@expo/vector-icons";
17 | import NoData from "@/components/NoData";
18 |
19 | const {Alist, AppInfo} = NativeModules
20 |
21 | const hash_salt = "https://github.com/alist-org/alist"
22 |
23 | export function hashPwd(pwd: string) {
24 | return sha256(`${pwd}-${hash_salt}`)
25 | }
26 |
27 | interface AListWebViewProps {
28 | path: string;
29 | }
30 |
31 | export interface AListWebviewRef {
32 | reload: WebView['reload'];
33 | getCurrentUrl: () => string;
34 | }
35 |
36 | const AListWebview = forwardRef((props: AListWebViewProps, forwardedRef: ForwardedRef) => {
37 | const { path } = props;
38 | const isRunning = useAppSelector(state => state.server.isRunning)
39 | const webviewRef = useRef(null)
40 | const navigation = useNavigation()
41 | const url = `http://127.0.0.1:5244${path}`
42 | const [injectedJS, setInjectedJS] = useState('')
43 | const [schemes, setSchemes] = useState([])
44 | const currentUrlRef = useRef(url)
45 |
46 | useImperativeHandle(forwardedRef, () => ({
47 | reload: () => {
48 | if (webviewRef.current) {
49 | webviewRef.current.reload();
50 | }
51 | },
52 | getCurrentUrl: () => currentUrlRef.current,
53 | }));
54 |
55 | const refreshWebToken = useCallback(async () => {
56 | if (isRunning) {
57 | try {
58 | const token = await Alist.getAdminToken()
59 | const script = `
60 | localStorage.setItem("token", "${token}");
61 | true;
62 | `
63 | // console.log('injectJavaScript', script)
64 | if (webviewRef.current) {
65 | webviewRef.current.injectJavaScript(script)
66 | } else {
67 | setInjectedJS(script)
68 | }
69 | } catch (e) {
70 | console.error(e)
71 | setInjectedJS('true')
72 | }
73 | }
74 | }, [isRunning])
75 |
76 | useLayoutEffect(() => {
77 | navigation.setOptions({
78 | headerLeft: isRunning ? () => (
79 | webviewRef.current?.reload()}
84 | style={{ marginLeft: 16 }}
85 | />
86 | ) : null,
87 | headerRight: isRunning ? () => (
88 | Linking.openURL(currentUrlRef.current)}
93 | style={{ marginRight: 16 }}
94 | />
95 | ) : null
96 | });
97 | }, [isRunning, injectedJS]);
98 |
99 | useFocusEffect(useCallback(() => {
100 | refreshWebToken()
101 | }, [refreshWebToken]));
102 |
103 | useEffect(() => {
104 | AppInfo.getApplicationQueriesSchemes().then(setSchemes)
105 | }, []);
106 |
107 | return isRunning ? injectedJS ? (
108 | {
116 | if (schemes.some(item => request.url.startsWith(`${item}:`))) {
117 | Linking.openURL(request.url).catch(err => {
118 | Toast.show('无法打开,请确认是否安装该App', {
119 | position: Toast.positions.CENTER,
120 | })
121 | });
122 | return false;
123 | }
124 | return true;
125 | }}
126 | applicationNameForUserAgent={'AListServer'}
127 | allowsBackForwardNavigationGestures={true}
128 | onNavigationStateChange={({url}) => currentUrlRef.current = url}
129 | onOpenWindow={({nativeEvent: {targetUrl}}) => {
130 | Linking.openURL(targetUrl)
131 | }}
132 | onFileDownload={({ nativeEvent: { downloadUrl } }) => {
133 | Linking.openURL(downloadUrl)
134 | }}
135 | onContentProcessDidTerminate={() => webviewRef.current?.reload()}
136 | startInLoadingState={true}
137 | renderLoading={() => }
138 | setBuiltInZoomControls={false}
139 | />
140 | ) : (
141 |
142 |
143 |
144 | ) : (
145 |
146 | );
147 | })
148 |
149 | export default AListWebview
150 |
--------------------------------------------------------------------------------
/components/Collapsible.tsx:
--------------------------------------------------------------------------------
1 | import Ionicons from '@expo/vector-icons/Ionicons';
2 | import { PropsWithChildren, useState } from 'react';
3 | import { StyleSheet, TouchableOpacity, useColorScheme } from 'react-native';
4 |
5 | import { ThemedText } from '@/components/ThemedText';
6 | import { ThemedView } from '@/components/ThemedView';
7 | import { Colors } from '@/constants/Colors';
8 |
9 | export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
10 | const [isOpen, setIsOpen] = useState(false);
11 | const theme = useColorScheme() ?? 'light';
12 |
13 | return (
14 |
15 | setIsOpen((value) => !value)}
18 | activeOpacity={0.8}>
19 |
24 | {title}
25 |
26 | {isOpen && {children}}
27 |
28 | );
29 | }
30 |
31 | const styles = StyleSheet.create({
32 | heading: {
33 | flexDirection: 'row',
34 | alignItems: 'center',
35 | gap: 6,
36 | },
37 | content: {
38 | marginTop: 6,
39 | marginLeft: 24,
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/components/ColorSchemeCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {View, useColorScheme, StyleSheet, ViewProps} from "react-native";
3 | import {Colors} from "@/constants/Colors";
4 |
5 | export default function ColorSchemeCard(props: ViewProps) {
6 | const colorScheme = useColorScheme()
7 | const backgroundColor = Colors[colorScheme ?? 'light'].cardBackgroundColor
8 |
9 | return (
10 |
11 | )
12 | }
13 |
14 |
15 | const styles = StyleSheet.create({
16 | card: {
17 | backgroundColor: 'white',
18 | paddingHorizontal: 20,
19 | borderRadius: 10,
20 | position: 'relative',
21 | },
22 | })
23 |
--------------------------------------------------------------------------------
/components/ColorSchemeText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {TextProps, Text, useColorScheme, StyleSheet} from "react-native";
3 | import {Colors} from "@/constants/Colors";
4 |
5 | export default function ColorSchemeText(props: TextProps) {
6 | const colorScheme = useColorScheme()
7 | const color = Colors[colorScheme ?? 'light'].text
8 |
9 | return (
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/components/ExternalLink.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'expo-router';
2 | import { openBrowserAsync } from 'expo-web-browser';
3 | import { type ComponentProps } from 'react';
4 | import { Platform } from 'react-native';
5 |
6 | type Props = Omit, 'href'> & { href: string };
7 |
8 | export function ExternalLink({ href, ...rest }: Props) {
9 | return (
10 | {
15 | if (Platform.OS !== 'web') {
16 | // Prevent the default behavior of linking to the default browser on native.
17 | event.preventDefault();
18 | // Open the link in an in-app browser.
19 | await openBrowserAsync(href);
20 | }
21 | }}
22 | />
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/HelloWave.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import Animated, {
3 | useSharedValue,
4 | useAnimatedStyle,
5 | withTiming,
6 | withRepeat,
7 | withSequence,
8 | } from 'react-native-reanimated';
9 |
10 | import { ThemedText } from '@/components/ThemedText';
11 |
12 | export function HelloWave() {
13 | const rotationAnimation = useSharedValue(0);
14 |
15 | rotationAnimation.value = withRepeat(
16 | withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
17 | 4 // Run the animation 4 times
18 | );
19 |
20 | const animatedStyle = useAnimatedStyle(() => ({
21 | transform: [{ rotate: `${rotationAnimation.value}deg` }],
22 | }));
23 |
24 | return (
25 |
26 | 👋
27 |
28 | );
29 | }
30 |
31 | const styles = StyleSheet.create({
32 | text: {
33 | fontSize: 28,
34 | lineHeight: 32,
35 | marginTop: -6,
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/components/NoData.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Image, View} from "react-native";
3 | import noDataImage from "@/assets/images/无服务.png";
4 | import Text from '@/components/ColorSchemeText'
5 |
6 | export default function NoData({text}: {text: string;}) {
7 | return (
8 |
9 |
10 | {text}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/components/ParallaxScrollView.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren, ReactElement } from 'react';
2 | import { StyleSheet, useColorScheme } from 'react-native';
3 | import Animated, {
4 | interpolate,
5 | useAnimatedRef,
6 | useAnimatedStyle,
7 | useScrollViewOffset,
8 | } from 'react-native-reanimated';
9 |
10 | import { ThemedView } from '@/components/ThemedView';
11 |
12 | const HEADER_HEIGHT = 250;
13 |
14 | type Props = PropsWithChildren<{
15 | headerImage: ReactElement;
16 | headerBackgroundColor: { dark: string; light: string };
17 | }>;
18 |
19 | export default function ParallaxScrollView({
20 | children,
21 | headerImage,
22 | headerBackgroundColor,
23 | }: Props) {
24 | const colorScheme = useColorScheme() ?? 'light';
25 | const scrollRef = useAnimatedRef();
26 | const scrollOffset = useScrollViewOffset(scrollRef);
27 |
28 | const headerAnimatedStyle = useAnimatedStyle(() => {
29 | return {
30 | transform: [
31 | {
32 | translateY: interpolate(
33 | scrollOffset.value,
34 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
35 | [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
36 | ),
37 | },
38 | {
39 | scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
40 | },
41 | ],
42 | };
43 | });
44 |
45 | return (
46 |
47 |
48 |
54 | {headerImage}
55 |
56 | {children}
57 |
58 |
59 | );
60 | }
61 |
62 | const styles = StyleSheet.create({
63 | container: {
64 | flex: 1,
65 | },
66 | header: {
67 | height: 250,
68 | overflow: 'hidden',
69 | },
70 | content: {
71 | flex: 1,
72 | padding: 32,
73 | gap: 16,
74 | overflow: 'hidden',
75 | },
76 | });
77 |
--------------------------------------------------------------------------------
/components/ThemedText.tsx:
--------------------------------------------------------------------------------
1 | import { Text, type TextProps, StyleSheet } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedTextProps = TextProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
9 | };
10 |
11 | export function ThemedText({
12 | style,
13 | lightColor,
14 | darkColor,
15 | type = 'default',
16 | ...rest
17 | }: ThemedTextProps) {
18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
19 |
20 | return (
21 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | default: {
38 | fontSize: 16,
39 | lineHeight: 24,
40 | },
41 | defaultSemiBold: {
42 | fontSize: 16,
43 | lineHeight: 24,
44 | fontWeight: '600',
45 | },
46 | title: {
47 | fontSize: 32,
48 | fontWeight: 'bold',
49 | lineHeight: 32,
50 | },
51 | subtitle: {
52 | fontSize: 20,
53 | fontWeight: 'bold',
54 | },
55 | link: {
56 | lineHeight: 30,
57 | fontSize: 16,
58 | color: '#0a7ea4',
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/components/ThemedView.tsx:
--------------------------------------------------------------------------------
1 | import { View, type ViewProps } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedViewProps = ViewProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | };
9 |
10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
12 |
13 | return ;
14 | }
15 |
--------------------------------------------------------------------------------
/components/__tests__/ThemedText-test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { ThemedText } from '../ThemedText';
5 |
6 | it(`renders correctly`, () => {
7 | const tree = renderer.create(Snapshot test!).toJSON();
8 |
9 | expect(tree).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/ThemedText-test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders correctly 1`] = `
4 |
22 | Snapshot test!
23 |
24 | `;
25 |
--------------------------------------------------------------------------------
/components/navigation/TabBarIcon.tsx:
--------------------------------------------------------------------------------
1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
2 |
3 | import Ionicons from '@expo/vector-icons/Ionicons';
4 | import { type IconProps } from '@expo/vector-icons/build/createIconSet';
5 | import { type ComponentProps } from 'react';
6 |
7 | export function TabBarIcon({ style, ...rest }: IconProps['name']>) {
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 | */
5 |
6 | const tintColorLight = '#0a7ea4';
7 | const tintColorDark = '#fff';
8 |
9 | export const Colors = {
10 | light: {
11 | text: '#11181C',
12 | background: '#fff',
13 | tint: tintColorLight,
14 | icon: '#687076',
15 | tabIconDefault: '#687076',
16 | tabIconSelected: tintColorLight,
17 | headerBackgroundColor: 'rgb(59, 112, 184)',
18 | cardBackgroundColor: 'white',
19 | },
20 | dark: {
21 | text: '#ECEDEE',
22 | background: '#151718',
23 | tint: tintColorDark,
24 | icon: '#9BA1A6',
25 | tabIconDefault: '#9BA1A6',
26 | tabIconSelected: tintColorDark,
27 | headerBackgroundColor: 'rgb(33, 33, 33)',
28 | cardBackgroundColor: 'rgb(28, 28, 28)',
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/hooks/useAppInActive.ts:
--------------------------------------------------------------------------------
1 | import {AppState, AppStateStatus} from "react-native";
2 | import {useEffect, useState} from "react";
3 |
4 | export default function useAppInActive() {
5 | const [inActive, setInActive] = useState(true);
6 |
7 | const handleAppStateChange = (nextAppState: AppStateStatus) => {
8 | if (nextAppState === 'active') {
9 | console.log('App切到前台了');
10 | setInActive(true);
11 | } else if (nextAppState === 'background') {
12 | console.log('App切到后台了');
13 | setInActive(false);
14 | }
15 | };
16 |
17 | useEffect(() => {
18 | const subscription = AppState.addEventListener('change', handleAppStateChange);
19 |
20 | return () => {
21 | subscription.remove()
22 | };
23 | }, []);
24 |
25 | return inActive
26 | }
27 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | export { useColorScheme } from 'react-native';
2 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | // NOTE: The default React Native styling doesn't support server rendering.
2 | // Server rendered styles should not change between the first render of the HTML
3 | // and the first render on the client. Typically, web developers will use CSS media queries
4 | // to render different styles on the client and server, these aren't directly supported in React Native
5 | // but can be achieved using a styling library like Nativewind.
6 | export function useColorScheme() {
7 | return 'light';
8 | }
9 |
--------------------------------------------------------------------------------
/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about light and dark modes:
3 | * https://docs.expo.dev/guides/color-schemes/
4 | */
5 |
6 | import { useColorScheme } from 'react-native';
7 |
8 | import { Colors } from '@/constants/Colors';
9 |
10 | export function useThemeColor(
11 | props: { light?: string; dark?: string },
12 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
13 | ) {
14 | // const theme = useColorScheme() ?? 'light';
15 | const theme = 'light';
16 | const colorFromProps = props[theme];
17 |
18 | if (colorFromProps) {
19 | return colorFromProps;
20 | } else {
21 | return Colors[theme][colorName];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/images.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg'
2 | declare module '*.png'
3 | declare module '*.jpg'
4 | declare module '*.jpeg'
5 | declare module '*.gif'
6 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 | .xcode.env.local
25 |
26 | # Bundle artifacts
27 | *.jsbundle
28 |
29 | # CocoaPods
30 | /Pods/
31 |
--------------------------------------------------------------------------------
/ios/.xcode.env:
--------------------------------------------------------------------------------
1 | # This `.xcode.env` file is versioned and is used to source the environment
2 | # used when running script phases inside Xcode.
3 | # To customize your local environment, you can create an `.xcode.env.local`
4 | # file that is not versioned.
5 |
6 | # NODE_BINARY variable contains the PATH to the node executable.
7 | #
8 | # Customize the NODE_BINARY variable here.
9 | # For example, to use nvm with brew, add the following line
10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use
11 | export NODE_BINARY=$(command -v node)
12 |
--------------------------------------------------------------------------------
/ios/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import React
3 | import Expo
4 | #if WITH_ICLOUD
5 | import CloudKit
6 | #endif
7 |
8 | @UIApplicationMain
9 | class AppDelegate: EXAppDelegateWrapper {
10 | override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
11 | self.moduleName = "main"
12 | self.initialProps = [String: Any]();
13 | application.registerForRemoteNotifications()
14 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
15 | }
16 |
17 | override func sourceURL(for bridge: RCTBridge!) -> URL! {
18 | return self.bundleURL()
19 | }
20 |
21 | override func bundleURL() -> URL {
22 | #if DEBUG
23 | return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry", fallbackExtension: nil)!
24 | #else
25 | return Bundle.main.url(forResource: "main", withExtension: "jsbundle")!
26 | #endif
27 | }
28 |
29 | override func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
30 | return super.application(application, open: url, options: options) || RCTLinkingManager.application(application, open: url, options: options)
31 | }
32 |
33 | override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
34 | let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
35 | return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
36 | }
37 |
38 | override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
39 | print("Did register for remote notifications")
40 | super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
41 | }
42 |
43 | override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
44 | print("ERROR: Failed to register for notifications: \(error.localizedDescription)")
45 | super.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
46 | }
47 |
48 | override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
49 | #if WITH_ICLOUD
50 | let cloudKitNotification = CKNotification(fromRemoteNotificationDictionary: userInfo)
51 |
52 | if(cloudKitNotification?.subscriptionID == "backup") {
53 | print("接收到iCloud远程通知")
54 | let iCloudSync: Bool = UserDefaults.standard.bool(forKey: "iCloudSync")
55 | if (iCloudSync) {
56 | CloudKitManager.shared.restore(resolve: {_ in
57 | }, reject: {_ in
58 | })
59 | }
60 | }
61 | #endif
62 | super.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
63 | }
64 |
65 | override func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
66 | return PlayerViewInstance.supportedInterfaceOrientations
67 | }
68 |
69 | override func applicationWillTerminate(_ application: UIApplication) {
70 | NotificationManager.shared.removeNotification()
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
3 |
4 | require 'json'
5 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
6 |
7 | ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
8 | ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
9 |
10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '13.4'
11 | install! 'cocoapods',
12 | :deterministic_uuids => false
13 |
14 | prepare_react_native_project!
15 |
16 | target 'alist' do
17 | use_expo_modules!
18 | config = use_native_modules!
19 |
20 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
21 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
22 | use_frameworks!
23 | pod 'KSPlayer',:git => 'https://github.com/kingslay/KSPlayer.git', :commit => '4d9b90066de3b976687fbc65372be0d8109675a7'
24 | pod 'FFmpegKit',:git => 'https://github.com/kingslay/FFmpegKit.git', :commit => '09368496e95d450c5abec61936edd467ed2e2436'
25 | pod 'OpenSSL',:git => 'https://github.com/kingslay/FFmpegKit.git', :commit => '09368496e95d450c5abec61936edd467ed2e2436'
26 |
27 | use_react_native!(
28 | :path => config[:reactNativePath],
29 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
30 | # An absolute path to your application root.
31 | :app_path => "#{Pod::Config.instance.installation_root}/..",
32 | # Temporarily disable privacy file aggregation by default, until React
33 | # Native 0.74.2 is released with fixes.
34 | :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] == 'true',
35 | )
36 |
37 | post_install do |installer|
38 | react_native_post_install(
39 | installer,
40 | config[:reactNativePath],
41 | :mac_catalyst_enabled => false,
42 | :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
43 | )
44 |
45 | # This is necessary for Xcode 14, because it signs resource bundles by default
46 | # when building for devices.
47 | installer.target_installation_results.pod_target_installation_results
48 | .each do |pod_name, target_installation_result|
49 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
50 | resource_bundle_target.build_configurations.each do |config|
51 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
52 | end
53 | end
54 | end
55 | end
56 |
57 | post_integrate do |installer|
58 | begin
59 | expo_patch_react_imports!(installer)
60 | rescue => e
61 | Pod::UI.warn e
62 | end
63 | end
64 |
65 | pre_install do |installer|
66 | installer.pod_targets.each do |pod|
67 | if pod.name.eql?('RNScreens') || pod.name.start_with?('RNReanimated')
68 | def pod.build_type
69 | Pod::BuildType.static_library
70 | end
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/ios/Podfile.properties.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo.jsEngine": "hermes",
3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true"
4 | }
5 |
--------------------------------------------------------------------------------
/ios/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryFileTimestamp
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | C617.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryUserDefaults
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | CA92.1
21 |
22 |
23 |
24 | NSPrivacyAccessedAPIType
25 | NSPrivacyAccessedAPICategorySystemBootTime
26 | NSPrivacyAccessedAPITypeReasons
27 |
28 | 35F9.1
29 |
30 |
31 |
32 | NSPrivacyCollectedDataTypes
33 |
34 | NSPrivacyTracking
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/alist copy-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleDisplayName
10 | AListServer
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
21 | CFBundleShortVersionString
22 | 1.23
23 | CFBundleSignature
24 | ????
25 | CFBundleURLTypes
26 |
27 |
28 | CFBundleURLSchemes
29 |
30 | myapp
31 | com.gendago.alist
32 |
33 |
34 |
35 | CFBundleVersion
36 | 1
37 | ITSAppUsesNonExemptEncryption
38 |
39 | LSApplicationQueriesSchemes
40 |
41 | senplayer
42 | iina
43 | potplayer
44 | vlc
45 | nplayer-http
46 | omniplayer
47 | figplayer
48 | infuse
49 | intent
50 | filebox
51 | open-vidhub
52 | yybx
53 |
54 | LSRequiresIPhoneOS
55 |
56 | LSSupportsOpeningDocumentsInPlace
57 |
58 | NSAppTransportSecurity
59 |
60 | NSAllowsArbitraryLoads
61 |
62 | NSAllowsLocalNetworking
63 |
64 |
65 | NSUserActivityTypes
66 |
67 | $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route
68 |
69 | UIBackgroundModes
70 |
71 | audio
72 | fetch
73 | remote-notification
74 |
75 | UIFileSharingEnabled
76 |
77 | UILaunchStoryboardName
78 | SplashScreen
79 | UIRequiredDeviceCapabilities
80 |
81 | arm64
82 |
83 | UIRequiresFullScreen
84 |
85 | UIStatusBarStyle
86 | UIStatusBarStyleDefault
87 | UISupportedInterfaceOrientations
88 |
89 | UIInterfaceOrientationPortrait
90 |
91 | UISupportedInterfaceOrientations~ipad
92 |
93 | UIInterfaceOrientationLandscapeLeft
94 | UIInterfaceOrientationLandscapeRight
95 | UIInterfaceOrientationPortrait
96 | UIInterfaceOrientationPortraitUpsideDown
97 |
98 | UIUserInterfaceStyle
99 | Automatic
100 | UIViewControllerBasedStatusBarAppearance
101 |
102 | LSMinimumSystemVersion
103 | 12.0
104 |
105 |
106 |
--------------------------------------------------------------------------------
/ios/alist.xcodeproj/xcshareddata/xcschemes/alist.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/ios/alist.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/alist.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/alist/Images.xcassets/AppIcon.appiconset/Alist-tinted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/ios/alist/Images.xcassets/AppIcon.appiconset/Alist-tinted.png
--------------------------------------------------------------------------------
/ios/alist/Images.xcassets/AppIcon.appiconset/Alist.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/ios/alist/Images.xcassets/AppIcon.appiconset/Alist.jpg
--------------------------------------------------------------------------------
/ios/alist/Images.xcassets/AppIcon.appiconset/Alist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/ios/alist/Images.xcassets/AppIcon.appiconset/Alist.png
--------------------------------------------------------------------------------
/ios/alist/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Alist.jpg",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | },
9 | {
10 | "appearances" : [
11 | {
12 | "appearance" : "luminosity",
13 | "value" : "dark"
14 | }
15 | ],
16 | "filename" : "Alist.png",
17 | "idiom" : "universal",
18 | "platform" : "ios",
19 | "size" : "1024x1024"
20 | },
21 | {
22 | "appearances" : [
23 | {
24 | "appearance" : "luminosity",
25 | "value" : "tinted"
26 | }
27 | ],
28 | "filename" : "Alist-tinted.png",
29 | "idiom" : "universal",
30 | "platform" : "ios",
31 | "size" : "1024x1024"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ios/alist/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/alist/Images.xcassets/SplashScreen.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "alist-splash.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ios/alist/Images.xcassets/SplashScreen.imageset/alist-splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/ios/alist/Images.xcassets/SplashScreen.imageset/alist-splash.png
--------------------------------------------------------------------------------
/ios/alist/Images.xcassets/SplashScreenBackground.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "background-black.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ios/alist/Images.xcassets/SplashScreenBackground.imageset/background-black.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/ios/alist/Images.xcassets/SplashScreenBackground.imageset/background-black.jpg
--------------------------------------------------------------------------------
/ios/alist/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleDisplayName
10 | AListServer
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
21 | CFBundleShortVersionString
22 | 1.23
23 | CFBundleSignature
24 | ????
25 | CFBundleURLTypes
26 |
27 |
28 | CFBundleURLSchemes
29 |
30 | myapp
31 | com.gendago.alist
32 |
33 |
34 |
35 | CFBundleVersion
36 | 1
37 | ITSAppUsesNonExemptEncryption
38 |
39 | LSApplicationQueriesSchemes
40 |
41 | senplayer
42 | iina
43 | potplayer
44 | vlc
45 | nplayer-http
46 | omniplayer
47 | figplayer
48 | infuse
49 | intent
50 | filebox
51 | open-vidhub
52 | yybx
53 |
54 | LSRequiresIPhoneOS
55 |
56 | LSSupportsOpeningDocumentsInPlace
57 |
58 | NSAppTransportSecurity
59 |
60 | NSAllowsArbitraryLoads
61 |
62 | NSAllowsLocalNetworking
63 |
64 |
65 | NSUserActivityTypes
66 |
67 | $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route
68 |
69 | UIBackgroundModes
70 |
71 | audio
72 | fetch
73 | remote-notification
74 |
75 | UIFileSharingEnabled
76 |
77 | UILaunchStoryboardName
78 | SplashScreen
79 | UIRequiredDeviceCapabilities
80 |
81 | arm64
82 |
83 | UIRequiresFullScreen
84 |
85 | UIStatusBarStyle
86 | UIStatusBarStyleDefault
87 | UISupportedInterfaceOrientations
88 |
89 | UIInterfaceOrientationPortrait
90 |
91 | UISupportedInterfaceOrientations~ipad
92 |
93 | UIInterfaceOrientationLandscapeLeft
94 | UIInterfaceOrientationLandscapeRight
95 | UIInterfaceOrientationPortrait
96 | UIInterfaceOrientationPortraitUpsideDown
97 |
98 | UIUserInterfaceStyle
99 | Automatic
100 | UIViewControllerBasedStatusBarAppearance
101 |
102 | LSMinimumSystemVersion
103 | 12.0
104 |
105 |
106 |
--------------------------------------------------------------------------------
/ios/alist/SplashScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/ios/alist/Supporting/Expo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | EXUpdatesCheckOnLaunch
6 | ALWAYS
7 | EXUpdatesEnabled
8 |
9 | EXUpdatesLaunchWaitMs
10 | 0
11 |
12 |
--------------------------------------------------------------------------------
/ios/alist/alist-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 | #import
5 |
--------------------------------------------------------------------------------
/ios/alist/alist.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.icloud-container-identifiers
8 |
9 | iCloud.com.gendago.alist
10 |
11 | com.apple.developer.icloud-services
12 |
13 | CloudKit
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/ios/alist/noop-file.swift:
--------------------------------------------------------------------------------
1 | //
2 | // @generated
3 | // A blank Swift file must be created for native modules with Swift files to work correctly.
4 | //
5 |
--------------------------------------------------------------------------------
/native/Alistlib/Alist.m:
--------------------------------------------------------------------------------
1 | // Alist.m
2 | #import
3 | #import
4 |
5 | @interface RCT_EXTERN_MODULE(Alist, NSObject)
6 |
7 | RCT_EXTERN_METHOD(init:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
8 |
9 | RCT_EXTERN_METHOD(start:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
10 |
11 | RCT_EXTERN_METHOD(stop:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
12 |
13 | RCT_EXTERN_METHOD(isRunning:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
14 |
15 | RCT_EXTERN_METHOD(setAdminPassword:(NSString *)password resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
16 |
17 | RCT_EXTERN_METHOD(getAdminPassword:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
18 |
19 | RCT_EXTERN_METHOD(getAdminUsername:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
20 |
21 | RCT_EXTERN_METHOD(getAdminToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
22 |
23 | RCT_EXTERN_METHOD(getOutboundIPString:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
24 |
25 | RCT_EXTERN_METHOD(iCloudRestore:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
26 |
27 | RCT_EXTERN_METHOD(iCloudBackup:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
28 |
29 | RCT_EXTERN_METHOD(iCloudSwitch:(BOOL)value resolve: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
30 |
31 | RCT_EXTERN_METHOD(setAutoStopHours:(nonnull NSNumber *)value resolve: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
32 |
33 | RCT_EXTERN_METHOD(getVersion:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
34 | @end
35 |
--------------------------------------------------------------------------------
/native/Alistlib/Alist.swift:
--------------------------------------------------------------------------------
1 | // GoLibraryBridge.swift
2 | import Foundation
3 | import React
4 | import Alistlib // 导入Go库
5 |
6 | class EventListener: NSObject, AlistlibEventProtocol {
7 | private var onProcessExitCallack: RCTResponseSenderBlock?
8 | private var onShutdownCallback: RCTResponseSenderBlock?
9 | private var onStartErrorCallback: RCTResponseSenderBlock?
10 |
11 | init(onProcessExit: @escaping RCTResponseSenderBlock, onShutdown: @escaping RCTResponseSenderBlock, onStartError: @escaping RCTResponseSenderBlock) {
12 | self.onProcessExitCallack = onProcessExit
13 | self.onShutdownCallback = onShutdown
14 | self.onStartErrorCallback = onStartError
15 | }
16 |
17 | func onProcessExit(_ code: Int) {
18 | self.onProcessExitCallack?([["code": code]])
19 | print("Process exited with code \(code).")
20 | }
21 |
22 | func onShutdown(_ t: String?) {
23 | self.onShutdownCallback?([["t": t]])
24 | print("Shutdown initiated: \(t ?? "No additional information")")
25 | }
26 |
27 | func onStartError(_ t: String?, err: String?) {
28 | self.onStartErrorCallback?([["t": t, "err": err]])
29 | print("Start error: \(err ?? "Unknown Error")")
30 | }
31 | }
32 |
33 | class LogCallbackHandler: NSObject, AlistlibLogCallbackProtocol {
34 | private var onLogCallback: RCTResponseSenderBlock? // 用于存储JavaScript的回调函数
35 |
36 | init(onLog: @escaping RCTResponseSenderBlock) {
37 | self.onLogCallback = onLog
38 | }
39 |
40 | func onLog(_ level: Int16, time: Int64, message: String?) {
41 | self.onLogCallback?([["level": level, "time": time, "message": message ?? ""]])
42 | print("Log message at level \(level): \(message ?? "No message")")
43 | }
44 | }
45 |
46 | class DataChangeCallbackHandler: NSObject, AlistlibDataChangeCallbackProtocol {
47 | var debounceTimer: Timer?
48 | func onChange(_ model: String?) {
49 | #if WITH_ICLOUD
50 | if (CloudKitManager.shared.restoring) {
51 | return
52 | }
53 | if (!UserDefaults.standard.bool(forKey: "iCloudSync")) {
54 | return
55 | }
56 | print("data onChange: \(String(describing: model))")
57 | DispatchQueue.main.async {
58 | self.debounceTimer?.invalidate()
59 | self.debounceTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false, block: { (timer) in
60 | let data = AlistlibBackup()
61 | CloudKitManager.shared.saveRecord(recordFields: ["json" : data]) { _ in
62 | print("备份数据成功")
63 | } reject: { msg in
64 | print("备份数据失败: \(String(describing: msg))")
65 | }
66 | })
67 | }
68 | #endif
69 | }
70 | }
71 |
72 | @objc(Alist)
73 | class Alist: RCTEventEmitter {
74 | @objc func `init`(_ resolve: @escaping RCTPromiseResolveBlock,
75 | rejecter reject: @escaping RCTPromiseRejectBlock) {
76 |
77 | let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
78 |
79 | let eventListener = EventListener(
80 | onProcessExit: { [weak self] (body) in
81 | self?.sendEvent(withName: "onProcessExit", body: body?[0])
82 | },
83 | onShutdown: { [weak self] (body) in
84 | self?.sendEvent(withName: "onShutdown", body: body?[0])
85 | },
86 | onStartError: { [weak self] (body) in
87 | self?.sendEvent(withName: "onStartError", body: body?[0])
88 | }
89 | )
90 |
91 | let logCallbackHandler = LogCallbackHandler(onLog: { [weak self] (logInfo) in
92 | self?.sendEvent(withName: "onLog", body: logInfo?[0])
93 | })
94 |
95 | // 初始化 NSError 的指针
96 | var error: NSError?
97 | AlistlibSetConfigData(documentsDirectory.path)
98 | AlistlibSetConfigLogStd(true)
99 | AlistlibInit(eventListener, logCallbackHandler, &error)
100 | if (error == nil) {
101 | resolve("ok")
102 | } else {
103 | reject("server start", "服务启动失败", error)
104 | }
105 | }
106 |
107 | @objc func start(_ resolve: @escaping RCTPromiseResolveBlock,
108 | rejecter reject: @escaping RCTPromiseRejectBlock) {
109 | let dataChangeCallbackHandler = DataChangeCallbackHandler()
110 | AlistlibStart(dataChangeCallbackHandler)
111 | resolve("ok")
112 | }
113 |
114 | @objc func stop(_ resolve: @escaping RCTPromiseResolveBlock,
115 | rejecter reject: @escaping RCTPromiseRejectBlock) {
116 | var error: NSError?
117 | AlistlibShutdown(0, &error)
118 |
119 | if (error == nil) {
120 | NotificationManager.shared.removeNotification()
121 | resolve("ok")
122 | } else {
123 | reject("server stop", "服务停止失败", error)
124 | }
125 | }
126 |
127 | @objc func isRunning(_ resolve: @escaping RCTPromiseResolveBlock,
128 | rejecter reject: @escaping RCTPromiseRejectBlock) {
129 | resolve(AlistlibIsRunning("http"))
130 | }
131 |
132 | @objc func getAdminPassword(_ resolve: @escaping RCTPromiseResolveBlock,
133 | rejecter reject: @escaping RCTPromiseRejectBlock) {
134 | resolve(AlistlibGetAdminPassword())
135 | }
136 |
137 | @objc func getAdminUsername(_ resolve: @escaping RCTPromiseResolveBlock,
138 | rejecter reject: @escaping RCTPromiseRejectBlock) {
139 | resolve(AlistlibGetAdminUsername())
140 | }
141 |
142 | @objc func getAdminToken(_ resolve: @escaping RCTPromiseResolveBlock,
143 | rejecter reject: @escaping RCTPromiseRejectBlock) {
144 | resolve(AlistlibGetAdminToken())
145 | }
146 |
147 | @objc func getOutboundIPString(_ resolve: @escaping RCTPromiseResolveBlock,
148 | rejecter reject: @escaping RCTPromiseRejectBlock) {
149 | resolve(AlistlibGetOutboundIPString())
150 | }
151 |
152 | @objc func setAdminPassword(_ password: String, resolver resolve: @escaping RCTPromiseResolveBlock,
153 | rejecter reject: @escaping RCTPromiseRejectBlock) {
154 | AlistlibSetAdminPassword(password)
155 | resolve("ok")
156 | }
157 |
158 | @objc func iCloudSwitch(_ value: Bool, resolve: @escaping RCTPromiseResolveBlock,
159 | rejecter reject: @escaping RCTPromiseRejectBlock) {
160 | UserDefaults.standard.set(value, forKey: "iCloudSync")
161 | }
162 |
163 | @objc func iCloudRestore(_ resolve: @escaping RCTPromiseResolveBlock,
164 | rejecter reject: @escaping RCTPromiseRejectBlock) {
165 | #if WITH_ICLOUD
166 | CloudKitManager.shared.restore { data in
167 | resolve(data)
168 | } reject: { msg in
169 | reject("iCloudError", msg, nil)
170 | }
171 | #else
172 | resolve("ok")
173 | #endif
174 | }
175 |
176 | @objc func iCloudBackup(_ resolve: @escaping RCTPromiseResolveBlock,
177 | rejecter reject: @escaping RCTPromiseRejectBlock) {
178 | #if WITH_ICLOUD
179 | CloudKitManager.shared.backup { data in
180 | resolve(data)
181 | } reject: { msg in
182 | reject("iCloudError", msg, nil)
183 | }
184 | #else
185 | resolve("ok")
186 | #endif
187 | }
188 |
189 | @objc func setAutoStopHours(_ value: NSNumber, resolve: @escaping RCTPromiseResolveBlock,
190 | rejecter reject: @escaping RCTPromiseRejectBlock) {
191 | AlistlibSetAutoStopHours(Int(value.intValue))
192 | }
193 |
194 | @objc func getVersion(_ resolve: @escaping RCTPromiseResolveBlock,
195 | rejecter reject: @escaping RCTPromiseRejectBlock) {
196 | resolve(AlistlibGetVersion())
197 | }
198 |
199 | // React Native桥接需要的因素
200 | @objc override static func requiresMainQueueSetup() -> Bool {
201 | return true
202 | }
203 |
204 | override func supportedEvents() -> [String]! {
205 | return ["onLog", "onProcessExit", "onShutdown", "onStartError"]
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/native/AppInfo/AppInfo.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface RCT_EXTERN_MODULE(AppInfo, NSObject)
4 | RCT_EXTERN_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve
5 | rejecter:(RCTPromiseRejectBlock)reject)
6 | RCT_EXTERN_METHOD(getApplicationQueriesSchemes:(RCTPromiseResolveBlock)resolve
7 | rejecter:(RCTPromiseRejectBlock)reject)
8 | RCT_EXTERN_METHOD(getWiFiAddress:(RCTPromiseResolveBlock)resolve
9 | rejecter:(RCTPromiseRejectBlock)reject)
10 | @end
11 |
--------------------------------------------------------------------------------
/native/AppInfo/AppInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import React
3 |
4 | @objc(AppInfo)
5 | class AppInfo: NSObject {
6 | @objc
7 | static func requiresMainQueueSetup() -> Bool {
8 | return true
9 | }
10 |
11 | @objc
12 | func getAppVersion(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
13 | if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
14 | resolve(version)
15 | } else {
16 | let error = NSError(domain: "", code: 200, userInfo: nil)
17 | reject("no_version", "No version found", error)
18 | }
19 | }
20 |
21 | @objc
22 | func getApplicationQueriesSchemes(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
23 | if let schemes = Bundle.main.infoDictionary?["LSApplicationQueriesSchemes"] as? [String] {
24 | resolve(schemes)
25 | } else {
26 | let error = NSError(domain: "", code: 200, userInfo: nil)
27 | reject("no_schemes", "No LSApplicationQueriesSchemes found", error)
28 | }
29 | }
30 |
31 | @objc
32 | func getWiFiAddress(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
33 | var en0Address: String?
34 | var en1Address: String?
35 | var en2Address: String?
36 | var ifaddr: UnsafeMutablePointer?
37 |
38 | // 获取网络接口列表
39 | if getifaddrs(&ifaddr) == 0 {
40 | var ptr = ifaddr
41 |
42 | while ptr != nil {
43 | defer { ptr = ptr?.pointee.ifa_next }
44 | guard let interface = ptr else {
45 | reject("-1", "no interface", nil)
46 | return
47 | }
48 |
49 | // 检查是否是 AF_INET(IPv4)
50 | let addrFamily = interface.pointee.ifa_addr.pointee.sa_family
51 | if addrFamily == UInt8(AF_INET) {
52 | let name = String(cString: interface.pointee.ifa_name)
53 | if name == "en0" || name == "en1" || name == "en2" {
54 | // 将 C 结构体指针转换为 Swift 的 sockaddr 结构体
55 | var addr = interface.pointee.ifa_addr.pointee
56 | var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
57 | if getnameinfo(&addr, socklen_t(interface.pointee.ifa_addr.pointee.sa_len),
58 | &hostname, socklen_t(hostname.count),
59 | nil, socklen_t(0), NI_NUMERICHOST) == 0 {
60 | let currentAddress = String(cString: hostname)
61 | if name == "en0" {
62 | en0Address = currentAddress
63 | break
64 | } else if name == "en1" {
65 | en1Address = currentAddress
66 | } else if name == "en2" {
67 | en2Address = currentAddress
68 | }
69 | }
70 | }
71 | }
72 | }
73 | freeifaddrs(ifaddr)
74 | }
75 |
76 | resolve(en0Address ?? en1Address ?? en2Address ?? "0.0.0.0")
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/native/BGRunManager/HCKeepBGRunManager.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface RCT_EXTERN_MODULE(HCKeepBGRunManager, NSObject)
4 |
5 | RCT_EXTERN_METHOD(startBGRun)
6 | RCT_EXTERN_METHOD(stopBGRun)
7 |
8 | @end
9 |
--------------------------------------------------------------------------------
/native/BGRunManager/HCKeepBGRunManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import AVFoundation
3 |
4 | @objc(HCKeepBGRunManager)
5 | class HCKeepBGRunManager: NSObject {
6 |
7 | var playerBack: AVAudioPlayer?
8 |
9 | @objc static let shared = HCKeepBGRunManager()
10 |
11 | override private init() {
12 | super.init()
13 | setupAudioSession()
14 | // 静音文件
15 | if let filePath = Bundle.main.path(forResource: "jm", ofType: "mp3"),
16 | let fileURL = URL(string: filePath) {
17 | do {
18 | playerBack = try AVAudioPlayer(contentsOf: fileURL)
19 | playerBack?.prepareToPlay()
20 | playerBack?.volume = 0.01
21 | playerBack?.numberOfLoops = -1 // 无限循环播放
22 | } catch {
23 | print("AVAudioPlayer init failed: \(error)")
24 | }
25 | }
26 | }
27 |
28 | func setupAudioSession() {
29 | let audioSession = AVAudioSession.sharedInstance()
30 | do {
31 | try audioSession.setCategory(.playback, options: .mixWithOthers) // 设置后台播放
32 | try audioSession.setActive(true)
33 | } catch {
34 | print("Error setting audio session category or activating it: \(error)")
35 | }
36 | }
37 |
38 | @objc func startBGRun() {
39 | playerBack?.play()
40 | }
41 |
42 | @objc func stopBGRun() {
43 | playerBack?.stop()
44 | }
45 |
46 | @objc static func moduleName() -> String! {
47 | return "HCKeepBGRunManager"
48 | }
49 |
50 | @objc static func requiresMainQueueSetup() -> Bool {
51 | return true
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/native/BGRunManager/jm.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/native/BGRunManager/jm.mp3
--------------------------------------------------------------------------------
/native/CloudKitManager/CloudKitManager.m:
--------------------------------------------------------------------------------
1 | // CloudKitManagerBridge.m
2 | #ifdef WITH_ICLOUD
3 | #import
4 |
5 | @interface RCT_EXTERN_MODULE(CloudKitManager, NSObject)
6 | RCT_EXTERN_METHOD(getUserRecordID: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject)
7 | @end
8 | #endif
9 |
--------------------------------------------------------------------------------
/native/CloudKitManager/CloudKitManager.swift:
--------------------------------------------------------------------------------
1 | // CloudKitManager.swift
2 | #if WITH_ICLOUD
3 | import Foundation
4 | import CloudKit
5 | import Alistlib
6 |
7 | @objc(CloudKitManager)
8 | class CloudKitManager: NSObject {
9 | private let container = CKContainer(identifier: "iCloud.com.gendago.alist")
10 | private var publicDB: CKDatabase {
11 | return container.privateCloudDatabase
12 | }
13 | private var userRecordId: String = "";
14 | private let recordType = "backup"
15 | public var restoring = false
16 | public static let shared = CloudKitManager()
17 |
18 | override init () {
19 | super.init()
20 | self.fetchUserRecordID()
21 | self.subscription()
22 | }
23 |
24 | @objc(getUserRecordID:rejecter:)
25 | func getUserRecordID(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
26 | container.fetchUserRecordID { (userRecordID, error) in
27 | if let error = error {
28 | reject("CloudKitError", error.localizedDescription, error)
29 | } else if let userRecordID = userRecordID {
30 | resolve(userRecordID.recordName)
31 | }
32 | }
33 | }
34 |
35 | func fetchUserRecordID(completion: (() -> Void)? = nil) {
36 | container.fetchUserRecordID { (userRecordID, error) in
37 | if let error = error {
38 | print(error.localizedDescription)
39 | } else if let userRecordID = userRecordID {
40 | self.userRecordId = userRecordID.recordName
41 | if let completion = completion {
42 | completion()
43 | }
44 | }
45 | }
46 | }
47 |
48 | func subscription() {
49 | let predicate = NSPredicate(value: true)
50 | let subscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: "backup", options: [CKQuerySubscription.Options.firesOnRecordDeletion, CKQuerySubscription.Options.firesOnRecordCreation, CKQuerySubscription.Options.firesOnRecordUpdate])
51 |
52 | let notificationInfo = CKSubscription.NotificationInfo()
53 | // 开启静默推送
54 | notificationInfo.shouldSendContentAvailable = true
55 | subscription.notificationInfo = notificationInfo
56 |
57 | publicDB.save(subscription) { (subscription, error) in
58 | print("已订阅数据推送")
59 | }
60 | }
61 |
62 | func saveRecord(recordFields: [String: Any], resolve: @escaping (Any) -> Void, reject: @escaping (String) -> Void) {
63 | if self.userRecordId != "" {
64 | let recordID = CKRecord.ID(recordName: "alist_backup")
65 |
66 | publicDB.fetch(withRecordID: recordID) { (fetchedRecord, error) in
67 | if let record = error == nil ? fetchedRecord : CKRecord(recordType: self.recordType, recordID: recordID) {
68 | recordFields.forEach { (key, value) in
69 | record[key] = value as? CKRecordValue
70 | }
71 |
72 | self.publicDB.save(record) { (record, error) in
73 | if let error = error {
74 | reject(error.localizedDescription)
75 | } else if let record = record {
76 | resolve(record.recordID.recordName)
77 | }
78 | }
79 | }
80 | }
81 | } else {
82 | self.fetchUserRecordID {
83 | self.saveRecord(recordFields: recordFields, resolve: resolve, reject: reject)
84 | }
85 | }
86 | }
87 |
88 | func queryRecord(options: NSDictionary?, resolve: @escaping (Any) -> Void, reject: @escaping (String) -> Void) {
89 | let query = CKQuery(recordType: recordType, predicate: NSPredicate(value: true))
90 |
91 | if let sortField = options?["sortField"] as? String, let ascending = options?["ascending"] as? Bool {
92 | query.sortDescriptors = [NSSortDescriptor(key: sortField, ascending: ascending)]
93 | }
94 |
95 | let queryOperation = CKQueryOperation(query: query)
96 | queryOperation.desiredKeys = nil // 返回所有字段
97 |
98 | var results = [[String: Any]]()
99 |
100 | queryOperation.recordFetchedBlock = { record in
101 | var dict = [String: Any]()
102 | for key in record.allKeys() {
103 | dict[key] = record[key]
104 | }
105 | results.append(dict)
106 | }
107 |
108 | queryOperation.queryCompletionBlock = { (cursor, error) in
109 | if let error = error {
110 | reject(error.localizedDescription)
111 | } else {
112 | resolve(results as Any)
113 | }
114 | }
115 |
116 | self.publicDB.add(queryOperation)
117 | }
118 |
119 | func restore(resolve: @escaping (Any) -> Void, reject: @escaping (String) -> Void) {
120 | self.restoring = true
121 | self.queryRecord(options: ["sortField": "creationDate", "ascending": false]) { data in
122 | if let records = data as? [[String: Any]], // 尝试转换类型
123 | let firstRecord = records.first, // 现在可以安全地调用.first
124 | let jsonString = firstRecord["json"] as? String { // 尝试从第一条记录中获取json字段
125 | AlistlibRestore(jsonString)
126 | print("恢复数据成功")
127 | resolve("ok")
128 | } else {
129 | print("数据格式错误或未查询到任何记录")
130 | reject("数据格式错误或未查询到任何记录")
131 | }
132 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
133 | // 避免恢复数据触发onChange事件导致重复备份
134 | self.restoring = false
135 | }
136 | } reject: { msg in
137 | print("查询icloud数据失败: \(String(describing: msg))")
138 | self.restoring = false
139 | reject(msg)
140 | }
141 | }
142 |
143 | func backup(resolve: @escaping (Any) -> Void, reject: @escaping (String) -> Void) {
144 | let data = AlistlibBackup()
145 | self.saveRecord(recordFields: ["json" : data]) { _ in
146 | print("备份数据成功")
147 | resolve("ok")
148 | } reject: { msg in
149 | print("备份数据失败: \(String(describing: msg))")
150 | reject(msg)
151 | }
152 | }
153 |
154 | }
155 | #endif
156 |
--------------------------------------------------------------------------------
/native/NotificationManager/NotificationManager.m:
--------------------------------------------------------------------------------
1 | #import "React/RCTBridgeModule.h"
2 |
3 | @interface RCT_EXTERN_MODULE(NotificationManager, NSObject)
4 |
5 | RCT_EXTERN_METHOD(requestAuthorization)
6 | RCT_EXTERN_METHOD(scheduleNotification:(NSString)title body:(NSString)body)
7 | RCT_EXTERN_METHOD(removeNotification)
8 |
9 | @end
10 |
--------------------------------------------------------------------------------
/native/NotificationManager/NotificationManager.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import UserNotifications
3 | import React
4 |
5 | @objc(NotificationManager)
6 | class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
7 |
8 | static let shared = NotificationManager()
9 |
10 | @objc func requestAuthorization() {
11 | let center = UNUserNotificationCenter.current()
12 | center.delegate = self
13 | center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
14 | if granted {
15 | print("Notification authorization granted.")
16 | } else {
17 | print("Notification authorization denied. Error: \(String(describing: error))")
18 | }
19 | }
20 | }
21 |
22 | @objc func scheduleNotification(_ title: String, body: String) {
23 | self.requestAuthorization()
24 |
25 | let content = UNMutableNotificationContent()
26 | content.title = title
27 | content.body = body
28 | content.sound = UNNotificationSound.default
29 |
30 | let request = UNNotificationRequest(identifier: "serverStatusNotification", content: content, trigger: nil)
31 |
32 | UNUserNotificationCenter.current().add(request) { error in
33 | if let error = error {
34 | print("Error adding notification: \(error.localizedDescription)")
35 | } else {
36 | print("Notification scheduled successfully!")
37 | }
38 | }
39 | }
40 |
41 | @objc func removeNotification() {
42 | UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ["serverStatusNotification"])
43 | print("Notification removed successfully!")
44 | }
45 |
46 | func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
47 | completionHandler([.alert, .sound]) // 显示通知横幅和声音
48 | }
49 |
50 | @objc static func requiresMainQueueSetup() -> Bool {
51 | return false
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/native/VideoPlayer/AudioPlayer.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface RCT_EXTERN_MODULE(AudioPlayer, RCTViewManager)
5 |
6 | RCT_EXTERN_METHOD(play:(nonnull NSString *)src header:(NSDictionary *)header)
7 |
8 | @end
9 |
--------------------------------------------------------------------------------
/native/VideoPlayer/AudioPlayer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 | import KSPlayer
4 |
5 | @objc(AudioPlayer)
6 | class AudioPlayer: NSObject {
7 |
8 | @objc func play(_ src: String, header: [String: String]) {
9 | DispatchQueue.main.async {
10 | if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
11 | let window = appDelegate.window
12 |
13 | if ((PlayerViewInstance.detailView) != nil) {
14 | PlayerViewInstance.detailView?.playerView.resetPlayer()
15 | PlayerViewInstance.detailView?.removeFromSuperview()
16 | PlayerViewInstance.detailView = nil
17 | }
18 |
19 | let options = KSOptions()
20 | if !header.isEmpty {
21 | options.appendHeader(header)
22 | }
23 | let controller = AudioView()
24 | controller.playerView.set(url: URL(string: src)!, options: options)
25 | controller.frame = window.bounds
26 | window.rootViewController?.view?.addSubview(controller)
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/native/VideoPlayer/AudioView.swift:
--------------------------------------------------------------------------------
1 | import CoreServices
2 | import KSPlayer
3 | import UIKit
4 |
5 | class AudioView: VideoView {
6 |
7 | override init() {
8 | super.init()
9 |
10 | KSOptions.canBackgroundPlay = true
11 | KSOptions.logLevel = .debug
12 | KSOptions.secondPlayerType = KSMEPlayer.self
13 | KSOptions.supportedInterfaceOrientations = .portrait
14 |
15 | self.addSubview(playerView)
16 | playerView.translatesAutoresizingMaskIntoConstraints = false
17 | playerView.landscapeButton.isHidden = true
18 | playerView.toolBar.pipButton.isHidden = true
19 | playerView.backBlock = { [weak self] in
20 | guard let self = self else { return }
21 | self.playerView.resetPlayer()
22 | self.removeFromSuperview()
23 | PlayerViewInstance.detailView = nil
24 | }
25 | playerView.becomeFirstResponder()
26 | playerView.delegate = self
27 | playerView.toolBar.playButton.isSelected = true
28 | playerView.replayButton.isHidden = true
29 | playerView.loadingIndector.isHidden = false
30 | playerView.loadingIndector.startAnimating()
31 | PlayerViewInstance.detailView = self
32 |
33 | NSLayoutConstraint.activate([
34 | playerView.topAnchor.constraint(equalTo: self.readableContentGuide.topAnchor),
35 | playerView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
36 | playerView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
37 | playerView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
38 | ])
39 |
40 | let imageView = UIImageView(image: UIImage(named: "audio"))
41 | imageView.contentMode = .scaleAspectFit
42 | imageView.translatesAutoresizingMaskIntoConstraints = false
43 | self.addSubview(imageView)
44 | NSLayoutConstraint.activate([
45 | imageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), // 水平居中
46 | imageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), // 垂直居中
47 | imageView.widthAnchor.constraint(equalToConstant: 200), // 图片宽度
48 | imageView.heightAnchor.constraint(equalToConstant: 200) // 图片高度
49 | ])
50 | }
51 |
52 | override func playerController(bufferedCount: Int, consumeTime: TimeInterval) {
53 | self.playerView.play()
54 | }
55 |
56 | required init?(coder: NSCoder) {
57 | fatalError("init(coder:) has not been implemented")
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/native/VideoPlayer/IOSView.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import KSPlayer
3 | import AVKit
4 |
5 | class IOSView: IOSVideoPlayerView {
6 | public var fullscreenCallback: ((_ isFullScreen: Bool) -> Void)?
7 | override func updateUI(isFullScreen: Bool) {
8 | super.updateUI(isFullScreen: isFullScreen)
9 | self.fullscreenCallback!(isFullScreen)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/native/VideoPlayer/PlayerViewInstance.swift:
--------------------------------------------------------------------------------
1 | import KSPlayer
2 |
3 | @objc(PlayerViewInstance)
4 | public class PlayerViewInstance: NSObject {
5 | static weak var detailView: VideoView?
6 | @objc public static var supportedInterfaceOrientations: UIInterfaceOrientationMask {
7 | if (KSOptions.supportedInterfaceOrientations == .allButUpsideDown) {
8 | return .portrait
9 | } else if let playerView = detailView?.playerView, playerView.landscapeButton.isSelected {
10 | if (playerView.playerLayer!.isPipActive) {
11 | return .portrait
12 | } else if (playerView.isHorizonal()) {
13 | return .landscape
14 | } else {
15 | return .portrait
16 | }
17 | } else {
18 | return KSOptions.supportedInterfaceOrientations
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/native/VideoPlayer/VideoPlayer.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface RCT_EXTERN_MODULE(VideoPlayer, RCTViewManager)
5 |
6 | RCT_EXTERN_METHOD(play:(nonnull NSString *)src header:(NSDictionary *)header)
7 |
8 | @end
9 |
--------------------------------------------------------------------------------
/native/VideoPlayer/VideoPlayer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 | import KSPlayer
4 |
5 | @objc(VideoPlayer)
6 | class VideoPlayer: NSObject {
7 |
8 | @objc func play(_ src: String, header: [String: String]) {
9 | DispatchQueue.main.async {
10 | if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
11 | let window = appDelegate.window
12 |
13 | if ((PlayerViewInstance.detailView) != nil) {
14 | PlayerViewInstance.detailView?.playerView.resetPlayer()
15 | PlayerViewInstance.detailView?.removeFromSuperview()
16 | PlayerViewInstance.detailView = nil
17 | }
18 |
19 | let options = KSOptions()
20 | if !header.isEmpty {
21 | options.appendHeader(header)
22 | if ((header["User-Agent"]) != nil) {
23 | options.userAgent = header["User-Agent"]
24 | }
25 | }
26 | let controller = VideoView()
27 | controller.playerView.set(url: URL(string: src)!, options: options)
28 | controller.frame = window.bounds
29 | window.rootViewController?.view?.addSubview(controller)
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/native/VideoPlayer/VideoView.swift:
--------------------------------------------------------------------------------
1 | import CoreServices
2 | import KSPlayer
3 | import UIKit
4 |
5 | class VideoView: UIView, PlayerControllerDelegate {
6 | let playerView = IOSView()
7 |
8 | init() {
9 | super.init(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
10 |
11 | KSOptions.canBackgroundPlay = true
12 | KSOptions.logLevel = .debug
13 | KSOptions.secondPlayerType = KSMEPlayer.self
14 | KSOptions.supportedInterfaceOrientations = .portrait
15 | KSOptions.isPipPopViewController = true
16 |
17 | self.addSubview(playerView)
18 | playerView.translatesAutoresizingMaskIntoConstraints = false
19 | playerView.backBlock = { [weak self] in
20 | guard let self = self else { return }
21 | self.playerView.resetPlayer()
22 | self.removeFromSuperview()
23 | PlayerViewInstance.detailView = nil
24 | }
25 | playerView.fullscreenCallback = { [weak self] isFullScreen in
26 | guard let self = self else { return }
27 | if (isFullScreen) {
28 | self.frame = CGRect(x: -1000, y: -1000, width: self.frame.width, height: self.frame.height)
29 | } else {
30 | self.playerView.resetPlayer()
31 | self.removeFromSuperview()
32 | PlayerViewInstance.detailView = nil
33 | }
34 | }
35 | playerView.becomeFirstResponder()
36 | playerView.delegate = self
37 | playerView.toolBar.playButton.isSelected = true
38 | playerView.replayButton.isHidden = true
39 | playerView.loadingIndector.isHidden = false
40 | playerView.loadingIndector.startAnimating()
41 | PlayerViewInstance.detailView = self
42 |
43 | NSLayoutConstraint.activate([
44 | playerView.topAnchor.constraint(equalTo: self.readableContentGuide.topAnchor),
45 | playerView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
46 | playerView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
47 | playerView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
48 | ])
49 | }
50 |
51 | func playerController(state: KSPlayer.KSPlayerState) {
52 | }
53 |
54 | func playerController(currentTime: TimeInterval, totalTime: TimeInterval) {
55 | }
56 |
57 | func playerController(finish error: Error?) {
58 | }
59 |
60 | func playerController(maskShow: Bool) {
61 | }
62 |
63 | func playerController(action: KSPlayer.PlayerButtonType) {
64 | }
65 |
66 | func playerController(bufferedCount: Int, consumeTime: TimeInterval) {
67 | self.playerView.updateUI(isFullScreen: true)
68 | self.playerView.play()
69 | }
70 |
71 | func playerController(seek: TimeInterval) {
72 | }
73 |
74 | required init?(coder: NSCoder) {
75 | fatalError("init(coder:) has not been implemented")
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/native/VideoPlayer/audio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gendago/alist-expo/ef1634dbcef2636ac7de6f9daf4b300af9595a2a/native/VideoPlayer/audio.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alist",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo run:android",
9 | "ios": "expo run:ios",
10 | "web": "expo start --web",
11 | "test": "jest --watchAll",
12 | "lint": "expo lint"
13 | },
14 | "jest": {
15 | "preset": "jest-expo"
16 | },
17 | "dependencies": {
18 | "@expo/vector-icons": "^14.0.0",
19 | "@react-native-async-storage/async-storage": "^1.23.1",
20 | "@react-native-clipboard/clipboard": "^1.14.1",
21 | "@react-native-community/netinfo": "^11.3.2",
22 | "@react-navigation/native": "^6.0.2",
23 | "@reduxjs/toolkit": "^2.2.5",
24 | "@types/sha256": "^0.2.2",
25 | "axios": "^1.7.2",
26 | "dayjs": "^1.11.11",
27 | "expo": "~51.0.6",
28 | "expo-constants": "~16.0.1",
29 | "expo-font": "~12.0.5",
30 | "expo-linking": "~6.3.1",
31 | "expo-router": "~3.5.12",
32 | "expo-splash-screen": "~0.27.4",
33 | "expo-status-bar": "~1.12.1",
34 | "expo-system-ui": "~3.0.4",
35 | "expo-web-browser": "~13.0.3",
36 | "react": "18.2.0",
37 | "react-dom": "18.2.0",
38 | "react-native": "0.74.1",
39 | "react-native-fs": "^2.20.0",
40 | "react-native-gesture-handler": "~2.16.1",
41 | "react-native-reanimated": "~3.10.1",
42 | "react-native-root-toast": "^3.6.0",
43 | "react-native-safe-area-context": "4.10.1",
44 | "react-native-screens": "3.31.1",
45 | "react-native-web": "~0.19.10",
46 | "react-native-webview": "^13.10.2",
47 | "react-redux": "^9.1.2",
48 | "redux-persist": "^6.0.0",
49 | "sha256": "^0.2.0"
50 | },
51 | "devDependencies": {
52 | "@babel/core": "^7.20.0",
53 | "@types/jest": "^29.5.12",
54 | "@types/react": "~18.2.45",
55 | "@types/react-test-renderer": "^18.0.7",
56 | "jest": "^29.2.1",
57 | "jest-expo": "~51.0.1",
58 | "react-test-renderer": "18.2.0",
59 | "typescript": "~5.3.3"
60 | },
61 | "private": true,
62 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
63 | }
64 |
--------------------------------------------------------------------------------
/scripts/reset-project.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * This script is used to reset the project to a blank state.
5 | * It moves the /app directory to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file.
6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it.
7 | */
8 |
9 | const fs = require('fs');
10 | const path = require('path');
11 |
12 | const root = process.cwd();
13 | const oldDirPath = path.join(root, 'app');
14 | const newDirPath = path.join(root, 'app-example');
15 | const newAppDirPath = path.join(root, 'app');
16 |
17 | const indexContent = `import { Text, View } from "react-native";
18 |
19 | export default function Index() {
20 | return (
21 |
28 | Edit app/index.tsx to edit this screen.
29 |
30 | );
31 | }
32 | `;
33 |
34 | const layoutContent = `import { Stack } from "expo-router";
35 |
36 | export default function RootLayout() {
37 | return (
38 |
39 |
40 |
41 | );
42 | }
43 | `;
44 |
45 | fs.rename(oldDirPath, newDirPath, (error) => {
46 | if (error) {
47 | return console.error(`Error renaming directory: ${error}`);
48 | }
49 | console.log('/app moved to /app-example.');
50 |
51 | fs.mkdir(newAppDirPath, { recursive: true }, (error) => {
52 | if (error) {
53 | return console.error(`Error creating new app directory: ${error}`);
54 | }
55 | console.log('New /app directory created.');
56 |
57 | const indexPath = path.join(newAppDirPath, 'index.tsx');
58 | fs.writeFile(indexPath, indexContent, (error) => {
59 | if (error) {
60 | return console.error(`Error creating index.tsx: ${error}`);
61 | }
62 | console.log('app/index.tsx created.');
63 |
64 | const layoutPath = path.join(newAppDirPath, '_layout.tsx');
65 | fs.writeFile(layoutPath, layoutContent, (error) => {
66 | if (error) {
67 | return console.error(`Error creating _layout.tsx: ${error}`);
68 | }
69 | console.log('app/_layout.tsx created.');
70 | });
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": [
7 | "./*"
8 | ]
9 | }
10 | },
11 | "include": [
12 | "**/*.ts",
13 | "**/*.tsx",
14 | ".expo/types/**/*.ts",
15 | "expo-env.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------