初始化项目

This commit is contained in:
2026-03-12 21:24:49 +08:00
commit c2b9c5d4c0
67 changed files with 8659 additions and 0 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.dockerignore
Dockerfile
build/
.dart_tool/
.git/
.github/
.gitignore
.idea/
.packages
data

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
data

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1490
.idea/caches/deviceStreaming.xml generated Normal file

File diff suppressed because it is too large Load Diff

420
.idea/libraries/Dart_Packages.xml generated Normal file
View File

@@ -0,0 +1,420 @@
<component name="libraryTable">
<library name="Dart Packages" type="DartPackagesLibraryType">
<properties>
<option name="packageNameToDirsMap">
<entry key="_fe_analyzer_shared">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/_fe_analyzer_shared-96.0.0/lib" />
</list>
</value>
</entry>
<entry key="analyzer">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/analyzer-10.2.0/lib" />
</list>
</value>
</entry>
<entry key="args">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/args-2.7.0/lib" />
</list>
</value>
</entry>
<entry key="async">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/async-2.13.0/lib" />
</list>
</value>
</entry>
<entry key="boolean_selector">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/boolean_selector-2.1.2/lib" />
</list>
</value>
</entry>
<entry key="cli_config">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/cli_config-0.2.0/lib" />
</list>
</value>
</entry>
<entry key="collection">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/collection-1.19.1/lib" />
</list>
</value>
</entry>
<entry key="convert">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/convert-3.1.2/lib" />
</list>
</value>
</entry>
<entry key="coverage">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/coverage-1.15.0/lib" />
</list>
</value>
</entry>
<entry key="crypto">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/crypto-3.0.7/lib" />
</list>
</value>
</entry>
<entry key="file">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/file-7.0.1/lib" />
</list>
</value>
</entry>
<entry key="frontend_server_client">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/frontend_server_client-4.0.0/lib" />
</list>
</value>
</entry>
<entry key="glob">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/glob-2.1.3/lib" />
</list>
</value>
</entry>
<entry key="http">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/http-1.6.0/lib" />
</list>
</value>
</entry>
<entry key="http_methods">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/http_methods-1.1.1/lib" />
</list>
</value>
</entry>
<entry key="http_multi_server">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/http_multi_server-3.2.2/lib" />
</list>
</value>
</entry>
<entry key="http_parser">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/http_parser-4.1.2/lib" />
</list>
</value>
</entry>
<entry key="io">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/io-1.0.5/lib" />
</list>
</value>
</entry>
<entry key="lints">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/lints-6.1.0/lib" />
</list>
</value>
</entry>
<entry key="logging">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/logging-1.3.0/lib" />
</list>
</value>
</entry>
<entry key="matcher">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/matcher-0.12.19/lib" />
</list>
</value>
</entry>
<entry key="meta">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/meta-1.18.1/lib" />
</list>
</value>
</entry>
<entry key="mime">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/mime-2.0.0/lib" />
</list>
</value>
</entry>
<entry key="node_preamble">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/node_preamble-2.0.2/lib" />
</list>
</value>
</entry>
<entry key="package_config">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/package_config-2.2.0/lib" />
</list>
</value>
</entry>
<entry key="path">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/path-1.9.1/lib" />
</list>
</value>
</entry>
<entry key="pool">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/pool-1.5.2/lib" />
</list>
</value>
</entry>
<entry key="pub_semver">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/pub_semver-2.2.0/lib" />
</list>
</value>
</entry>
<entry key="shelf">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf-1.4.2/lib" />
</list>
</value>
</entry>
<entry key="shelf_packages_handler">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf_packages_handler-3.0.2/lib" />
</list>
</value>
</entry>
<entry key="shelf_router">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf_router-1.1.4/lib" />
</list>
</value>
</entry>
<entry key="shelf_static">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf_static-1.1.3/lib" />
</list>
</value>
</entry>
<entry key="shelf_web_socket">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf_web_socket-3.0.0/lib" />
</list>
</value>
</entry>
<entry key="source_map_stack_trace">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/source_map_stack_trace-2.1.2/lib" />
</list>
</value>
</entry>
<entry key="source_maps">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/source_maps-0.10.13/lib" />
</list>
</value>
</entry>
<entry key="source_span">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/source_span-1.10.2/lib" />
</list>
</value>
</entry>
<entry key="stack_trace">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/stack_trace-1.12.1/lib" />
</list>
</value>
</entry>
<entry key="stream_channel">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/stream_channel-2.1.4/lib" />
</list>
</value>
</entry>
<entry key="string_scanner">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/string_scanner-1.4.1/lib" />
</list>
</value>
</entry>
<entry key="term_glyph">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/term_glyph-1.2.2/lib" />
</list>
</value>
</entry>
<entry key="test">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/test-1.30.0/lib" />
</list>
</value>
</entry>
<entry key="test_api">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/test_api-0.7.10/lib" />
</list>
</value>
</entry>
<entry key="test_core">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/test_core-0.6.16/lib" />
</list>
</value>
</entry>
<entry key="typed_data">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/typed_data-1.4.0/lib" />
</list>
</value>
</entry>
<entry key="vm_service">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/vm_service-15.0.2/lib" />
</list>
</value>
</entry>
<entry key="watcher">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/watcher-1.2.1/lib" />
</list>
</value>
</entry>
<entry key="web">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/web-1.1.1/lib" />
</list>
</value>
</entry>
<entry key="web_socket">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/web_socket-1.0.1/lib" />
</list>
</value>
</entry>
<entry key="web_socket_channel">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/web_socket_channel-3.0.3/lib" />
</list>
</value>
</entry>
<entry key="webkit_inspection_protocol">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/webkit_inspection_protocol-1.2.1/lib" />
</list>
</value>
</entry>
<entry key="yaml">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/yaml-3.1.3/lib" />
</list>
</value>
</entry>
</option>
</properties>
<CLASSES>
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/_fe_analyzer_shared-96.0.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/analyzer-10.2.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/args-2.7.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/async-2.13.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/boolean_selector-2.1.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/cli_config-0.2.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/collection-1.19.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/convert-3.1.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/coverage-1.15.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/crypto-3.0.7/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/file-7.0.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/frontend_server_client-4.0.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/glob-2.1.3/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/http-1.6.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/http_methods-1.1.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/http_multi_server-3.2.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/http_parser-4.1.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/io-1.0.5/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/lints-6.1.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/logging-1.3.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/matcher-0.12.19/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/meta-1.18.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/mime-2.0.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/node_preamble-2.0.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/package_config-2.2.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/path-1.9.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/pool-1.5.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/pub_semver-2.2.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf-1.4.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf_packages_handler-3.0.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf_router-1.1.4/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf_static-1.1.3/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/shelf_web_socket-3.0.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/source_map_stack_trace-2.1.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/source_maps-0.10.13/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/source_span-1.10.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/stack_trace-1.12.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/stream_channel-2.1.4/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/string_scanner-1.4.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/term_glyph-1.2.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/test-1.30.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/test_api-0.7.10/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/test_core-0.6.16/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/typed_data-1.4.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/vm_service-15.0.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/watcher-1.2.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/web-1.1.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/web_socket-1.0.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/web_socket_channel-3.0.3/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/webkit_inspection_protocol-1.2.1/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/mirrors.tuna.tsinghua.edu.cn%47dart-pub%47/yaml-3.1.3/lib" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

31
.idea/libraries/Dart_SDK.xml generated Normal file
View File

@@ -0,0 +1,31 @@
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/_internal" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/async" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/cli" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/collection" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/concurrent" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/convert" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/core" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/developer" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/ffi" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/html" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/indexed_db" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/io" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/isolate" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/js" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/js_interop" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/js_interop_unsafe" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/js_util" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/math" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/mirrors" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/svg" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/typed_data" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/web_audio" />
<root url="file://$PROJECT_DIR$/../../../Repository/Scoop/apps/dart/3.10.7/lib/web_gl" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

8
.idea/markdown.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

5
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/loongyan.iml" filepath="$PROJECT_DIR$/loongyan.iml" />
</modules>
</component>
</project>

6
.idea/studiobot.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedOut" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

3
CHANGELOG.md Normal file
View File

@@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# Use latest stable channel SDK.
FROM dart:stable AS build
# Resolve app dependencies.
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
# Copy app source code (except anything in .dockerignore) and AOT compile app.
COPY . .
RUN dart compile exe bin/server.dart -o bin/server
# Build minimal serving image from AOT-compiled `/server`
# and the pre-built AOT-runtime in the `/runtime/` directory of the base image.
FROM scratch
COPY --from=build /runtime/ /
COPY --from=build /app/bin/server /app/bin/
# Start server.
EXPOSE 8080
CMD ["/app/bin/server"]

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
A server app built using [Shelf](https://pub.dev/packages/shelf),
configured to enable running with [Docker](https://www.docker.com/).
This sample code handles HTTP GET requests to `/` and `/echo/<message>`
# Running the sample
## Running with the Dart SDK
You can run the example with the [Dart SDK](https://dart.dev/get-dart)
like this:
```
$ dart run bin/server.dart
Server listening on port 8080
```
And then from a second terminal:
```
$ curl http://0.0.0.0:8080
Hello, World!
$ curl http://0.0.0.0:8080/echo/I_love_Dart
I_love_Dart
```
## Running with Docker
If you have [Docker Desktop](https://www.docker.com/get-started) installed, you
can build and run with the `docker` command:
```
$ docker build . -t myserver
$ docker run -it -p 8080:8080 myserver
Server listening on port 8080
```
And then from a second terminal:
```
$ curl http://0.0.0.0:8080
Hello, World!
$ curl http://0.0.0.0:8080/echo/I_love_Dart
I_love_Dart
```
You should see the logging printed in the first terminal:
```
2021-05-06T15:47:04.620417 0:00:00.000158 GET [200] /
2021-05-06T15:47:08.392928 0:00:00.001216 GET [200] /echo/I_love_Dart
```

30
analysis_options.yaml Normal file
View File

@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

View File

@@ -0,0 +1,22 @@
class Album {
final int id;
final String name;
final DateTime createdAt;
final DateTime updatedAt;
Album({
required this.id,
required this.name,
required this.createdAt,
required this.updatedAt,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
}

View File

@@ -0,0 +1,37 @@
class Photo {
final int id;
final int albumId;
final String filePath;
final String fileName;
final int fileSize;
final String mimeType;
final int? width;
final int? height;
final DateTime createdAt;
Photo({
required this.id,
required this.albumId,
required this.filePath,
required this.fileName,
required this.fileSize,
required this.mimeType,
this.width,
this.height,
required this.createdAt,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'albumId': albumId,
'filePath': filePath,
'fileName': fileName,
'fileSize': fileSize,
'mimeType': mimeType,
'width': width,
'height': height,
'createdAt': createdAt.toIso8601String(),
};
}
}

View File

@@ -0,0 +1,41 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import '../entities/album.dart';
class AlbumRepository {
final String basePath;
AlbumRepository({required this.basePath});
Future<List<Album>> getAllAlbums() async {
final dir = Directory(basePath);
if (!await dir.exists()) {
return [];
}
var albums = await dir
.list()
.where((d) => d is Directory)
.map((d) => d as Directory)
.map((dir) {
final name = p.basename(dir.path);
return Album(
id: name.hashCode,
name: name,
createdAt: dir.statSync().changed,
updatedAt: dir.statSync().changed,
);
})
.toList();
return albums;
}
Future<Album?> getAlbumById(int id) async {
final albums = await getAllAlbums();
return albums.where((a) => a.id == id).firstOrNull;
}
}

View File

@@ -0,0 +1,317 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:path/path.dart' as p;
import '../entities/photo.dart';
class PhotoRepository {
final String basePath;
PhotoRepository(this.basePath);
Future<List<Photo>> getPhotosByAlbumId(int id) async {
final dir = Directory(basePath);
if (!await dir.exists()) {
return [];
}
Directory? albumDir;
try {
albumDir = await dir
.list()
.where((f) => f is Directory)
.where((d) => p.basename(d.path).hashCode == id)
.first
as Directory;
} on StateError {
return [];
}
// 获取相册中的所有文件
final files = await albumDir
.list()
.where((f) => f is File)
.map((f) => f as File)
.where((f) => _isImageOrVideo(f.path))
.toList();
// 并行处理所有文件的尺寸获取
final photoFutures = files.map((file) async {
final stat = file.statSync();
final fileName = p.basename(file.path);
final mimeType = _getMimeType(file.path);
final dimensions = _getImageDimensions(file, mimeType);
return Photo(
id: fileName.hashCode,
albumId: id,
filePath: file.path,
fileName: fileName,
fileSize: stat.size,
mimeType: mimeType,
width: dimensions?.$1,
height: dimensions?.$2,
createdAt: stat.changed,
);
});
return Future.wait(photoFutures);
}
Future<Photo?> getPhotoById(int id) async {
final dir = Directory(basePath);
if (!await dir.exists()) {
return null;
}
File file;
try {
file = await dir
.list(recursive: true)
.where((f) => f is File)
.map((f) => f as File)
.firstWhere((f) => p.basename(f.path).hashCode == id);
} on StateError {
// firstWhere throws StateError when no element is found
return null;
}
final stat = file.statSync();
final mimeType = _getMimeType(file.path);
final dimensions = _getImageDimensions(file, mimeType);
return Photo(
id: p.basename(file.path).hashCode,
albumId: p.basename(file.parent.path).hashCode,
filePath: file.path,
fileName: p.basename(file.path),
fileSize: stat.size,
mimeType: mimeType,
width: dimensions?.$1,
height: dimensions?.$2,
createdAt: stat.changed,
);
}
/// 获取图片的宽度和高度
/// 返回 (width, height) 或 null如果是视频或无法解析
(int, int)? _getImageDimensions(File file, String mimeType) {
// 视频文件不返回尺寸
if (mimeType.startsWith('video/')) {
return null;
}
try {
// 只读取文件头部的一小部分来获取尺寸
final raf = file.openSync();
try {
// 读取前 32KB 应该足够获取图片尺寸
final bytesToRead = 32 * 1024;
final buffer = Uint8List(bytesToRead);
final bytesRead = raf.readIntoSync(buffer);
final data = buffer.sublist(0, bytesRead);
return _parseImageDimensions(data, mimeType);
} finally {
raf.closeSync();
}
} catch (_) {
// 如果无法解析图片,返回 null
return null;
}
}
/// 解析图片尺寸
/// 支持 JPEG, PNG, GIF, BMP, WebP 格式
(int, int)? _parseImageDimensions(Uint8List data, String mimeType) {
try {
switch (mimeType) {
case 'image/jpeg':
return _parseJpegDimensions(data);
case 'image/png':
return _parsePngDimensions(data);
case 'image/gif':
return _parseGifDimensions(data);
case 'image/bmp':
return _parseBmpDimensions(data);
case 'image/webp':
return _parseWebpDimensions(data);
default:
return null;
}
} catch (_) {
return null;
}
}
/// 解析 JPEG 图片尺寸
(int, int)? _parseJpegDimensions(Uint8List data) {
if (data.length < 2 || data[0] != 0xFF || data[1] != 0xD8) {
return null; // 不是有效的 JPEG
}
var offset = 2;
while (offset < data.length - 1) {
if (data[offset] != 0xFF) {
offset++;
continue;
}
final marker = data[offset + 1];
// SOF 标记 (Start Of Frame)
if (marker >= 0xC0 && marker <= 0xC3 ||
marker >= 0xC5 && marker <= 0xC7 ||
marker >= 0xC9 && marker <= 0xCB ||
marker >= 0xCD && marker <= 0xCF) {
if (offset + 9 < data.length) {
final height = (data[offset + 5] << 8) | data[offset + 6];
final width = (data[offset + 7] << 8) | data[offset + 8];
return (width, height);
}
}
// 跳过这个标记
if (marker >= 0xD0 && marker <= 0xD9) {
offset += 2;
} else if (marker == 0xFF) {
offset += 2;
} else {
if (offset + 3 < data.length) {
final length = (data[offset + 2] << 8) | data[offset + 3];
offset += 2 + length;
} else {
break;
}
}
}
return null;
}
/// 解析 PNG 图片尺寸
(int, int)? _parsePngDimensions(Uint8List data) {
if (data.length < 24 ||
data[0] != 0x89 || data[1] != 0x50 ||
data[2] != 0x4E || data[3] != 0x47 ||
data[4] != 0x0D || data[5] != 0x0A ||
data[6] != 0x1A || data[7] != 0x0A) {
return null; // 不是有效的 PNG
}
// IHDR chunk 在第 16-23 字节
final width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19];
final height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23];
return (width, height);
}
/// 解析 GIF 图片尺寸
(int, int)? _parseGifDimensions(Uint8List data) {
if (data.length < 10 ||
data[0] != 0x47 || data[1] != 0x49 ||
data[2] != 0x46 || data[3] != 0x38) {
return null; // 不是有效的 GIF
}
final width = data[6] | (data[7] << 8);
final height = data[8] | (data[9] << 8);
return (width, height);
}
/// 解析 BMP 图片尺寸
(int, int)? _parseBmpDimensions(Uint8List data) {
if (data.length < 26 || data[0] != 0x42 || data[1] != 0x4D) {
return null; // 不是有效的 BMP
}
final width = data[18] | (data[19] << 8) | (data[20] << 16) | (data[21] << 24);
final height = data[22] | (data[23] << 8) | (data[24] << 16) | (data[25] << 24);
return (width, height.abs()); // BMP 高度可能是负数
}
/// 解析 WebP 图片尺寸
(int, int)? _parseWebpDimensions(Uint8List data) {
if (data.length < 30 ||
data[0] != 0x52 || data[1] != 0x49 ||
data[2] != 0x46 || data[3] != 0x46 ||
data[8] != 0x57 || data[9] != 0x45 ||
data[10] != 0x42 || data[11] != 0x50) {
return null; // 不是有效的 WebP
}
final type = String.fromCharCodes(data.sublist(12, 16));
if (type == 'VP8 ') {
// Lossy WebP
if (data.length < 30) return null;
final key = (data[23] << 8) | data[22];
if (key != 0x9D01) return null;
final width = data[26] | (data[27] << 8);
final height = data[28] | (data[29] << 8);
return (width, height);
} else if (type == 'VP8L') {
// Lossless WebP
if (data.length < 29) return null;
if (data[21] != 0x2F) return null;
final width = 1 + ((data[22] | (data[23] << 8)) & 0x3FFF);
final height = 1 + (((data[23] >> 6) | (data[24] << 2) | (data[25] << 10)) & 0x3FFF);
return (width, height);
} else if (type == 'VP8X') {
// Extended WebP
if (data.length < 30) return null;
final width = 1 + (data[24] | (data[25] << 8) | (data[26] << 16));
final height = 1 + (data[27] | (data[28] << 8) | (data[29] << 16));
return (width, height);
}
return null;
}
Future<void> deletePhoto(int id) {
// TODO: implement deletePhoto
throw UnimplementedError();
}
bool _isImageOrVideo(String path) {
final ext = p.extension(path).toLowerCase();
return [
'.jpg',
'.jpeg',
'.png',
'.gif',
'.bmp',
'.webp',
'.mp4',
'.avi',
'.mov',
'.mkv',
].contains(ext);
}
String _getMimeType(String path) {
final ext = p.extension(path).toLowerCase();
switch (ext) {
case '.jpg':
case '.jpeg':
return 'image/jpeg';
case '.png':
return 'image/png';
case '.gif':
return 'image/gif';
case '.bmp':
return 'image/bmp';
case '.webp':
return 'image/webp';
case '.mp4':
return 'video/mp4';
case '.avi':
return 'video/avi';
case '.mov':
return 'video/quicktime';
case '.mkv':
return 'video/x-matroska';
default:
return 'application/octet-stream';
}
}
}

View File

@@ -0,0 +1,43 @@
import 'package:shelf/shelf.dart';
import 'dart:io';
/// 全局异常处理中间件
/// 捕获所有未处理的异常并返回适当的 HTTP 响应
Middleware exceptionHandler() {
return (Handler innerHandler) {
return (Request request) async {
try {
return await innerHandler(request);
} on FormatException catch (e) {
// 处理参数格式错误,如无效的 ID
return Response.badRequest(
body: 'Invalid parameter format: ${e.message}',
headers: {'content-type': 'text/plain'},
);
} on PathNotFoundException catch (e) {
// 处理路径未找到
return Response.notFound('Resource not found: ${e.message}');
} on FileSystemException catch (e) {
// 处理文件系统错误
return Response.internalServerError(
body: 'File system error: ${e.message}',
headers: {'content-type': 'text/plain'},
);
} on Exception catch (e) {
// 处理其他已知异常
return Response.internalServerError(
body: 'Internal server error: ${e.toString()}',
headers: {'content-type': 'text/plain'},
);
} catch (e, stackTrace) {
// 处理未知异常
print('Unhandled exception: $e');
print('Stack trace: $stackTrace');
return Response.internalServerError(
body: 'An unexpected error occurred',
headers: {'content-type': 'text/plain'},
);
}
};
};
}

138
bin/router.dart Normal file
View File

@@ -0,0 +1,138 @@
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'domain/repositories/photo_repository.dart';
import 'domain/repositories/album_repository.dart';
final albumRepository = AlbumRepository(basePath: 'data');
final photoRepository = PhotoRepository('data');
Response jsonResponse(dynamic data) {
return Response.ok(
jsonEncode(data),
headers: {'content-type': 'application/json'},
);
}
Router createRouter() {
final router = Router()
..get('/', _rootHandler)
..get("/album", _listAlbumHandler)
..get("/album/<id>", _getAlbumHandler)
..get("/album/<id>/photo", _getPhotosOfAlbumHander)
..get('/photo/<id>', _getPhotoHandler)
..get('/photo/<id>/file', _getPhotoFileHandler)
..get('/echo/<message>', _echoHandler);
return router;
}
Response _rootHandler(Request req) {
return Response.ok('Hello, World!\n');
}
Response _echoHandler(Request request) {
final message = request.params['message'];
return Response.ok('$message\n');
}
Future<Response> _listAlbumHandler(Request req) async {
final albums = await albumRepository.getAllAlbums();
final albumList = albums
.map(
(a) => {
'id': a.id,
'name': a.name,
'createdAt': a.createdAt.toIso8601String(),
'updatedAt': a.updatedAt.toIso8601String(),
},
)
.toList();
return jsonResponse(albumList);
}
Future<Response> _getAlbumHandler(Request req) async {
final idParam = req.params['id'];
if (idParam == null) {
return Response.badRequest(body: 'Missing album id');
}
final id = int.tryParse(idParam);
if (id == null) {
return Response.badRequest(body: 'Invalid album id: $idParam');
}
final album = await albumRepository.getAlbumById(id);
if (album == null) {
return Response.notFound('Album not found');
}
return jsonResponse({
'id': album.id,
'name': album.name,
});
}
Future<Response> _getPhotoHandler(Request req) async {
final idParam = req.params['id'];
if (idParam == null) {
return Response.badRequest(body: 'Missing photo id');
}
final id = int.tryParse(idParam);
if (id == null) {
return Response.badRequest(body: 'Invalid photo id: $idParam');
}
final photo = await photoRepository.getPhotoById(id);
if (photo == null) {
return Response.notFound('Photo not found');
}
return jsonResponse(photo);
}
Future<Response> _getPhotoFileHandler(Request req) async {
final idParam = req.params['id'];
if (idParam == null) {
return Response.badRequest(body: 'Missing photo id');
}
final id = int.tryParse(idParam);
if (id == null) {
return Response.badRequest(body: 'Invalid photo id: $idParam');
}
final photo = await photoRepository.getPhotoById(id);
if (photo == null) {
return Response.notFound('Photo not found');
}
final filePath = photo.filePath;
final file = File(filePath);
if (!await file.exists()) {
return Response.notFound('File not found');
}
return Response.ok(
file.openRead(),
headers: {
'Content-Type': photo.mimeType,
'Content-Length': photo.fileSize.toString(),
},
);
}
Future<Response> _getPhotosOfAlbumHander(Request req) async {
final idParam = req.params['id'];
if (idParam == null) {
return Response.badRequest(body: 'Missing album id');
}
final albumId = int.tryParse(idParam);
if (albumId == null) {
return Response.badRequest(body: 'Invalid album id: $idParam');
}
final photos = await photoRepository.getPhotosByAlbumId(albumId);
return jsonResponse(photos);
}

23
bin/server.dart Normal file
View File

@@ -0,0 +1,23 @@
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'router.dart';
import 'middleware/exception_handler.dart';
void main(List<String> args) async {
// Use any available host or container IP (usually `0.0.0.0`).
final ip = InternetAddress.anyIPv4;
// Configure a pipeline that logs requests and handles exceptions.
final handler = Pipeline()
.addMiddleware(logRequests())
.addMiddleware(exceptionHandler())
.addHandler(createRouter().call);
// For running in containers, we respect the PORT environment variable.
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final server = await serve(handler, ip, port);
print('Server listening on port ${server.port}');
}

14
loongyan.iml Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

413
pubspec.lock Normal file
View File

@@ -0,0 +1,413 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "96.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "10.2.0"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.2"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.2.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.15.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.0.7"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.3"
http:
dependency: "direct dev"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.6.0"
http_methods:
dependency: transitive
description:
name: http_methods
sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.1.1"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.0.5"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.12.19"
meta:
dependency: transitive
description:
name: meta
sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.18.1"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.2.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.2.0"
shelf:
dependency: "direct main"
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.0.2"
shelf_router:
dependency: "direct main"
description:
name: shelf_router
sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.1.4"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.30.0"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.7.10"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.6.16"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.0-0 <4.0.0"

17
pubspec.yaml Normal file
View File

@@ -0,0 +1,17 @@
name: loongyan
description: A server app using the shelf package and Docker.
version: 1.0.0
# repository: https://github.com/my_org/my_repo
environment:
sdk: ^3.10.7
dependencies:
shelf: ^1.4.2
shelf_router: ^1.1.2
path: ^1.9.0
dev_dependencies:
http: ^1.2.2
lints: ^6.0.0
test: ^1.25.6

179
test/server_test.dart Normal file
View File

@@ -0,0 +1,179 @@
import 'dart:io';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
void main() {
final port = '8080';
final host = 'http://localhost:$port';
late Process p;
setUp(() async {
p = await Process.start(
'dart',
['run', 'bin/server.dart'],
environment: {'PORT': port},
);
// Wait for server to start (give it some time to initialize)
await Future.delayed(const Duration(seconds: 3));
});
tearDown(() async {
p.kill();
// Wait for process to exit
try {
await p.exitCode.timeout(const Duration(seconds: 5), onTimeout: () => 0);
} catch (_) {}
});
group('Root endpoint', () {
test('GET / returns Hello World', () async {
final response = await http.get(Uri.parse('$host/'));
expect(response.statusCode, 200);
expect(response.body, 'Hello, World!\n');
});
});
group('Echo endpoint', () {
test('GET /echo/<message> returns the message', () async {
final response = await http.get(Uri.parse('$host/echo/hello'));
expect(response.statusCode, 200);
expect(response.body, 'hello\n');
});
test('GET /echo/<message> with special characters', () async {
final response = await http.get(Uri.parse('$host/echo/test%20message'));
expect(response.statusCode, 200);
expect(response.body, contains('test'));
});
});
group('Album endpoints', () {
test('GET /album returns list of albums', () async {
final response = await http.get(Uri.parse('$host/album'));
expect(response.statusCode, 200);
expect(response.headers['content-type'], contains('application/json'));
final albums = jsonDecode(response.body) as List;
expect(albums, isA<List>());
if (albums.isNotEmpty) {
expect(albums.first, containsPair('id', isA<int>()));
expect(albums.first, containsPair('name', isA<String>()));
}
});
test('GET /album/<id> with invalid id returns 400', () async {
final response = await http.get(Uri.parse('$host/album/invalid_id'));
expect(response.statusCode, 400);
expect(response.body, contains('Invalid album id'));
});
test('GET /album/<id> with non-existent id returns 404', () async {
final response = await http.get(Uri.parse('$host/album/999999'));
expect(response.statusCode, 404);
expect(response.body, contains('Album not found'));
});
test('GET /album/<id>/photo with invalid id returns 400', () async {
final response = await http.get(Uri.parse('$host/album/invalid_id/photo'));
expect(response.statusCode, 400);
expect(response.body, contains('Invalid album id'));
});
test('GET /album/<id>/photo with valid id returns photos', () async {
// First get the list to find a valid album id
final listResponse = await http.get(Uri.parse('$host/album'));
expect(listResponse.statusCode, 200);
final albums = jsonDecode(listResponse.body) as List;
// Skip test if no albums exist
if (albums.isEmpty) {
return;
}
final album = albums.first as Map<String, dynamic>;
final albumId = album['id'];
final response = await http.get(Uri.parse('$host/album/$albumId/photo'));
expect(response.statusCode, 200);
expect(response.headers['content-type'], contains('application/json'));
final photos = jsonDecode(response.body) as List;
expect(photos, isA<List>());
});
});
group('Photo endpoints', () {
test('GET /photo/<id> with invalid id returns 400', () async {
final response = await http.get(Uri.parse('$host/photo/abc'));
expect(response.statusCode, 400);
expect(response.body, contains('Invalid photo id'));
});
test('GET /photo/<id> with non-existent id returns 404', () async {
// Use a hash that won't match any file
final response = await http.get(Uri.parse('$host/photo/12345'));
expect(response.statusCode, 404);
expect(response.body, contains('Photo not found'));
});
test('GET /photo/<id>/file with invalid id returns 400', () async {
final response = await http.get(Uri.parse('$host/photo/abc/file'));
expect(response.statusCode, 400);
expect(response.body, contains('Invalid photo id'));
});
test('GET /photo/<id>/file with non-existent id returns 404', () async {
// Use a hash that won't match any file
final response = await http.get(Uri.parse('$host/photo/12345/file'));
expect(response.statusCode, 404);
expect(response.body, contains('Photo not found'));
});
test('GET /photo/<id>/file with valid id returns file', () async {
// First get a valid photo id from an album
final albumResponse = await http.get(Uri.parse('$host/album'));
expect(albumResponse.statusCode, 200);
final albums = jsonDecode(albumResponse.body) as List;
// Skip test if no albums exist
if (albums.isEmpty) {
return;
}
final album = albums.first as Map<String, dynamic>;
final albumId = album['id'];
final photosResponse = await http.get(
Uri.parse('$host/album/$albumId/photo'),
);
expect(photosResponse.statusCode, 200);
final photos = jsonDecode(photosResponse.body) as List;
// Skip test if no photos exist
if (photos.isEmpty) {
return;
}
final photo = photos.first as Map<String, dynamic>;
final photoId = photo['id'];
final response = await http.get(
Uri.parse('$host/photo/$photoId/file'),
);
expect(response.statusCode, 200);
expect(response.contentLength, greaterThan(0));
});
});
group('404 handling', () {
test('Unknown route returns 404', () async {
final response = await http.get(Uri.parse('$host/foobar'));
expect(response.statusCode, 404);
});
test('Unknown nested route returns 404', () async {
final response = await http.get(Uri.parse('$host/api/v1/unknown'));
expect(response.statusCode, 404);
});
});
}

9
web/.env.example Normal file
View File

@@ -0,0 +1,9 @@
# Drizzle
DATABASE_URL=local.db
ORIGIN=""
# Better Auth
# For production use 32 characters and generated with high entropy
# https://www.better-auth.com/docs/installation
BETTER_AUTH_SECRET=""

28
web/.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Paraglide
src/lib/paraglide
project.inlang/cache/
# SQLite
*.db

1
web/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

10
web/.prettierignore Normal file
View File

@@ -0,0 +1,10 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
/drizzle/

15
web/.prettierrc Normal file
View File

@@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

3
web/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
}

42
web/README.md Normal file
View File

@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
pnpm dlx sv@0.12.6 create --template minimal --types jsdoc --add prettier vitest="usages:unit,component" eslint sveltekit-adapter="adapter:auto" devtools-json better-auth="demo:password" mdsvex paraglide="languageTags:en, zh+demo:yes" drizzle="database:sqlite+sqlite:better-sqlite3" --install pnpm web/
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

11
web/drizzle.config.js Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'drizzle-kit';
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
export default defineConfig({
schema: './src/lib/server/db/schema.js',
dialect: 'sqlite',
dbCredentials: { url: process.env.DATABASE_URL },
verbose: true,
strict: true
});

28
web/eslint.config.js Normal file
View File

@@ -0,0 +1,28 @@
import prettier from 'eslint-config-prettier';
import path from 'node:path';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import svelteConfig from './svelte.config.js';
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
export default defineConfig(
[
includeIgnoreFile(gitignorePath),
js.configs.recommended,
svelte.configs.recommended,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } }
},
{
files: ['**/*.svelte', '**/*.svelte.js'],
languageOptions: { parserOptions: { svelteConfig } }
}
],
prettier,
svelte.configs.prettier
);

19
web/jsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

4
web/messages/en.json Normal file
View File

@@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from en!"
}

4
web/messages/zh.json Normal file
View File

@@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from zh!"
}

49
web/package.json Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "web",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"auth:schema": "better-auth generate --config src/lib/server/auth.js --output src/lib/server/db/auth.schema.js --yes"
},
"devDependencies": {
"@better-auth/cli": "~1.4.21",
"@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2",
"@inlang/paraglide-js": "^2.10.0",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22",
"@vitest/browser-playwright": "^4.0.18",
"better-auth": "~1.4.21",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^17.3.0",
"mdsvex": "^0.12.6",
"playwright": "^1.58.2",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"svelte": "^5.51.0",
"svelte-check": "^4.4.2",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-devtools-json": "^1.0.0",
"vitest": "^4.0.18",
"vitest-browser-svelte": "^2.0.2"
},
"dependencies": {
}
}

4711
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
web/pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
},
"baseLocale": "en",
"locales": ["en", "zh"]
}

19
web/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
import type { User, Session } from 'better-auth/minimal';
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
interface Locals {
user?: User;
session?: Session;
}
// interface Error {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

15
web/src/app.html Normal file
View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="%paraglide.lang%" dir="%paraglide.dir%">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

3
web/src/hooks.js Normal file
View File

@@ -0,0 +1,3 @@
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute = (request) => deLocalizeUrl(request.url).pathname;

40
web/src/hooks.server.js Normal file
View File

@@ -0,0 +1,40 @@
import { sequence } from '@sveltejs/kit/hooks';
import { building } from '$app/environment';
import { auth } from '$lib/server/auth';
import { svelteKitHandler } from 'better-auth/svelte-kit';
import { getTextDirection } from '$lib/paraglide/runtime';
import { paraglideMiddleware } from '$lib/paraglide/server';
/** @type {import('@sveltejs/kit').Handle} */ const handleParaglide = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request, locale }) => {
event.request = request;
return resolve(event, {
transformPageChunk: ({ html }) =>
html
.replace('%paraglide.lang%', locale)
.replace('%paraglide.dir%', getTextDirection(locale))
});
});
/** @type {import('@sveltejs/kit').Handle} */ const handleBetterAuth = async ({
event,
resolve
}) => {
const session = await auth.api.getSession({
/** @type {import('@sveltejs/kit').Handle} */ headers: event.request.headers
});
if (session) {
event.locals.session = session.session;
event.locals.user = session.user;
}
return svelteKitHandler({ event, resolve, auth, building });
};
export /** @type {import('@sveltejs/kit').Handle} */ const handle = sequence(
handleParaglide,
handleBetterAuth
);
/** @type {import('@sveltejs/kit').Handle} */

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
web/src/lib/index.js Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,13 @@
import { betterAuth } from 'better-auth/minimal';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { sveltekitCookies } from 'better-auth/svelte-kit';
import { env } from '$env/dynamic/private';
import { getRequestEvent } from '$app/server';
export const auth = betterAuth({
baseURL: env.ORIGIN,
secret: env.BETTER_AUTH_SECRET,
// database: drizzleAdapter(db, { provider: 'sqlite' }),
emailAndPassword: { enabled: true },
plugins: [sveltekitCookies(getRequestEvent)] // make sure this is the last plugin in the array
});

View File

@@ -0,0 +1 @@
// If you see this file, you have not run the auth:schema script yet, but you should!

View File

@@ -0,0 +1,10 @@
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';
import { env } from '$env/dynamic/private';
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
const client = new Database(env.DATABASE_URL);
export const db = drizzle(client, { schema });

View File

@@ -0,0 +1,11 @@
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const task = sqliteTable('task', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
title: text('title').notNull(),
priority: integer('priority').notNull().default(1)
});
export * from './auth.schema';

View File

@@ -0,0 +1,8 @@
<script>
import { greet } from './greet';
let { host = 'SvelteKit', guest = 'Vitest' } = $props();
</script>
<h1>{greet(host)}</h1>
<p>{greet(guest)}</p>

View File

@@ -0,0 +1,15 @@
import { page } from 'vitest/browser';
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import Welcome from './Welcome.svelte';
describe('Welcome.svelte', () => {
it('renders greetings for host and guest', async () => {
render(Welcome, { host: 'SvelteKit', guest: 'Vitest' });
await expect
.element(page.getByRole('heading', { level: 1 }))
.toHaveTextContent('Hello, SvelteKit!');
await expect.element(page.getByText('Hello, Vitest!')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,3 @@
export function greet(name) {
return 'Hello, ' + name + '!';
}

View File

@@ -0,0 +1,8 @@
import { describe, it, expect } from 'vitest';
import { greet } from './greet';
describe('greet', () => {
it('returns a greeting', () => {
expect(greet('Svelte')).toBe('Hello, Svelte!');
});
});

View File

@@ -0,0 +1,16 @@
<script>
import { page } from '$app/state';
import { locales, localizeHref } from '$lib/paraglide/runtime';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
{@render children()}
<div style="display:none">
{#each locales as locale}
<a href={localizeHref(page.url.pathname, { locale })}>{locale}</a>
{/each}
</div>

View File

@@ -0,0 +1,2 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>

View File

@@ -0,0 +1,6 @@
<script>
import { resolve } from '$app/paths';
</script>
<a href={resolve('/demo/better-auth')}>better-auth</a>
<a href={resolve('/demo/paraglide')}>paraglide</a>

View File

@@ -0,0 +1,19 @@
import { redirect } from '@sveltejs/kit';
import { auth } from '$lib/server/auth';
export const load = async (event) => {
if (!event.locals.user) {
return redirect(302, '/demo/better-auth/login');
}
return { user: event.locals.user };
};
export const actions = {
signOut: async (event) => {
await auth.api.signOut({
headers: event.request.headers
});
return redirect(302, '/demo/better-auth/login');
}
};

View File

@@ -0,0 +1,11 @@
<script>
import { enhance } from '$app/forms';
let { data } = $props();
</script>
<h1>Hi, {data.user.name}!</h1>
<p>Your user ID is {data.user.id}.</p>
<form method="post" action="?/signOut" use:enhance>
<button>Sign out</button>
</form>

View File

@@ -0,0 +1,60 @@
import { fail, redirect } from '@sveltejs/kit';
import { auth } from '$lib/server/auth';
import { APIError } from 'better-auth/api';
export const load = async (event) => {
if (event.locals.user) {
return redirect(302, '/demo/better-auth');
}
return {};
};
export const actions = {
signInEmail: async (event) => {
const formData = await event.request.formData();
const email = formData.get('email')?.toString() ?? '';
const password = formData.get('password')?.toString() ?? '';
try {
await auth.api.signInEmail({
body: {
email,
password,
callbackURL: '/auth/verification-success'
}
});
} catch (error) {
if (error instanceof APIError) {
return fail(400, { message: error.message || 'Signin failed' });
}
return fail(500, { message: 'Unexpected error' });
}
return redirect(302, '/demo/better-auth');
},
signUpEmail: async (event) => {
const formData = await event.request.formData();
const email = formData.get('email')?.toString() ?? '';
const password = formData.get('password')?.toString() ?? '';
const name = formData.get('name')?.toString() ?? '';
try {
await auth.api.signUpEmail({
body: {
email,
password,
name,
callbackURL: '/auth/verification-success'
}
});
} catch (error) {
if (error instanceof APIError) {
return fail(400, { message: error.message || 'Registration failed' });
}
return fail(500, { message: 'Unexpected error' });
}
return redirect(302, '/demo/better-auth');
}
};

View File

@@ -0,0 +1,24 @@
<script>
import { enhance } from '$app/forms';
let { form } = $props();
</script>
<h1>Login</h1>
<form method="post" action="?/signInEmail" use:enhance>
<label>
Email
<input type="email" name="email" />
</label>
<label>
Password
<input type="password" name="password" />
</label>
<label>
Name (for registration)
<input name="name" />
</label>
<button>Login</button>
<button formaction="?/signUpEmail">Register</button>
</form>
<p style="color: red">{form?.message ?? ''}</p>

View File

@@ -0,0 +1,22 @@
<script>
import { setLocale } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages.js';
</script>
<h1>{m.hello_world({ name: 'SvelteKit User' })}</h1>
<div>
<button onclick={() => setLocale('en')}>en</button>
<button onclick={() => setLocale('zh')}>zh</button>
</div>
<p>
If you use VSCode, install the
<a
href="https://marketplace.visualstudio.com/items?itemName=inlang.vs-code-extension"
target="_blank">Sherlock i18n extension</a
>
for a better i18n experience.
</p>

3
web/static/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:

19
web/svelte.config.js Normal file
View File

@@ -0,0 +1,19 @@
import { mdsvex } from 'mdsvex';
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
},
vitePlugin: {
dynamicCompileOptions: ({ filename }) => ({ runes: !filename.includes('node_modules') })
},
preprocess: [mdsvex()],
extensions: ['.svelte', '.svx']
};
export default config;

41
web/vite.config.js Normal file
View File

@@ -0,0 +1,41 @@
import { paraglideVitePlugin } from '@inlang/paraglide-js';
import devtoolsJson from 'vite-plugin-devtools-json';
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
plugins: [
sveltekit(),
devtoolsJson(),
paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide' })
],
test: {
expect: { requireAssertions: true },
projects: [
{
extends: './vite.config.js',
test: {
name: 'client',
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium', headless: true }]
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
exclude: ['src/lib/server/**']
}
},
{
extends: './vite.config.js',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
}
}
]
}
});