mirror of
https://github.com/simon-ding/polaris.git
synced 2026-03-19 09:47:27 +08:00
feat: add system page
This commit is contained in:
@@ -19,6 +19,7 @@ const (
|
||||
IndexerTorznabImpl = "torznab"
|
||||
DataPath = "./data"
|
||||
ImgPath = DataPath + "/img"
|
||||
LogPath = DataPath + "/logs"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
13
pkg/uptime/uptime.go
Normal file
13
pkg/uptime/uptime.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package uptime
|
||||
|
||||
import "time"
|
||||
|
||||
var startTime time.Time
|
||||
|
||||
func Uptime() time.Duration {
|
||||
return time.Since(startTime)
|
||||
}
|
||||
|
||||
func init() {
|
||||
startTime = time.Now()
|
||||
}
|
||||
@@ -50,12 +50,13 @@ func (s *Server) Serve() error {
|
||||
s.r.Use(ginzap.RecoveryWithZap(log.Logger().Desugar(), true))
|
||||
|
||||
log.SetLogLevel(s.db.GetSetting(db.SettingLogLevel)) //restore log level
|
||||
|
||||
|
||||
s.r.POST("/api/login", HttpHandler(s.Login))
|
||||
|
||||
api := s.r.Group("/api/v1")
|
||||
api.Use(s.authModdleware)
|
||||
api.StaticFS("/img", http.Dir(db.ImgPath))
|
||||
api.StaticFS("/logs", http.Dir(db.LogPath))
|
||||
api.Any("/posters/*proxyPath", s.proxyPosters)
|
||||
|
||||
setting := api.Group("/setting")
|
||||
@@ -64,6 +65,8 @@ func (s *Server) Serve() error {
|
||||
setting.GET("/general", HttpHandler(s.GetSetting))
|
||||
setting.POST("/auth", HttpHandler(s.EnableAuth))
|
||||
setting.GET("/auth", HttpHandler(s.GetAuthSetting))
|
||||
setting.GET("/logfiles", HttpHandler(s.GetAllLogs))
|
||||
setting.GET("/about", HttpHandler(s.About))
|
||||
}
|
||||
activity := api.Group("/activity")
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
|
||||
return &GeneralSettings{
|
||||
TmdbApiKey: tmdb,
|
||||
DownloadDir: downloadDir,
|
||||
LogLevel: logLevel,
|
||||
LogLevel: logLevel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
52
server/systems.go
Normal file
52
server/systems.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"polaris/db"
|
||||
"polaris/log"
|
||||
"polaris/pkg/uptime"
|
||||
"runtime"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type LogFile struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func (s *Server) GetAllLogs(c *gin.Context) (interface{}, error) {
|
||||
fs, err := os.ReadDir(db.LogPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read log dir")
|
||||
}
|
||||
var logs []LogFile
|
||||
for _, f := range fs {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := f.Info()
|
||||
if err != nil {
|
||||
log.Warnf("get log file error: %v", err)
|
||||
continue
|
||||
}
|
||||
l := LogFile{
|
||||
Name: f.Name(),
|
||||
Size: info.Size(),
|
||||
}
|
||||
logs = append(logs, l)
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (s *Server) About(c *gin.Context) (interface{}, error) {
|
||||
|
||||
return gin.H{
|
||||
"intro": "Polaris © Simon Ding",
|
||||
"homepage": "https://github.com/simon-ding/polaris",
|
||||
"uptime": uptime.Uptime(),
|
||||
"chat_group": "https://t.me/+8R2nzrlSs2JhMDgx",
|
||||
"go_version": runtime.Version(),
|
||||
}, nil
|
||||
}
|
||||
169
ui/lib/main.dart
169
ui/lib/main.dart
@@ -8,7 +8,8 @@ import 'package:ui/login_page.dart';
|
||||
import 'package:ui/movie_watchlist.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/search.dart';
|
||||
import 'package:ui/system_settings.dart';
|
||||
import 'package:ui/settings.dart';
|
||||
import 'package:ui/system_page.dart';
|
||||
import 'package:ui/tv_details.dart';
|
||||
import 'package:ui/welcome_page.dart';
|
||||
|
||||
@@ -26,8 +27,6 @@ class MyApp extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _MyAppState extends ConsumerState<MyApp> {
|
||||
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -35,8 +34,9 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||
final shellRoute = ShellRoute(
|
||||
builder: (BuildContext context, GoRouterState state, Widget child) {
|
||||
return SelectionArea(
|
||||
child: MainSkeleton(body: Padding(padding: const EdgeInsets.all(20), child: child),
|
||||
),
|
||||
child: MainSkeleton(
|
||||
body: Padding(padding: const EdgeInsets.all(20), child: child),
|
||||
),
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
@@ -74,6 +74,10 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||
GoRoute(
|
||||
path: ActivityPage.route,
|
||||
builder: (context, state) => const ActivityPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: SystemPage.route,
|
||||
builder: (context, state) => const SystemPage(),
|
||||
)
|
||||
],
|
||||
);
|
||||
@@ -95,7 +99,9 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||
theme: ThemeData(
|
||||
fontFamily: "NotoSansSC",
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blueAccent, brightness: Brightness.dark, surface: Colors.black54),
|
||||
seedColor: Colors.blueAccent,
|
||||
brightness: Brightness.dark,
|
||||
surface: Colors.black54),
|
||||
useMaterial3: true,
|
||||
//scaffoldBackgroundColor: Color.fromARGB(255, 26, 24, 24)
|
||||
),
|
||||
@@ -103,7 +109,6 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MainSkeleton extends StatefulWidget {
|
||||
@@ -130,85 +135,85 @@ class _MainSkeletonState extends State<MainSkeleton> {
|
||||
_selectedTab = 2;
|
||||
} else if (uri.contains(SystemSettingsPage.route)) {
|
||||
_selectedTab = 3;
|
||||
} else if (uri.contains(SystemPage.route)) {
|
||||
_selectedTab = 4;
|
||||
}
|
||||
|
||||
return AdaptiveScaffold(
|
||||
appBarBreakpoint: Breakpoints.standard,
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: const Row(
|
||||
children: [
|
||||
Text("Polaris"),
|
||||
],
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: const Row(
|
||||
children: [
|
||||
Text("Polaris"),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
SearchAnchor(
|
||||
builder: (BuildContext context, SearchController controller) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 40),
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: SearchBar(
|
||||
hintText: "搜索...",
|
||||
leading: const Icon(Icons.search),
|
||||
controller: controller,
|
||||
shadowColor: WidgetStateColor.transparent,
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
Theme.of(context).colorScheme.primaryContainer),
|
||||
onSubmitted: (value) => context.go(Uri(
|
||||
path: SearchPage.route,
|
||||
queryParameters: {'query': value}).toString()),
|
||||
),
|
||||
actions: [
|
||||
SearchAnchor(builder:
|
||||
(BuildContext context, SearchController controller) {
|
||||
return Container(
|
||||
constraints:
|
||||
const BoxConstraints(maxWidth: 300, maxHeight: 40),
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: SearchBar(
|
||||
hintText: "搜索...",
|
||||
leading: const Icon(Icons.search),
|
||||
controller: controller,
|
||||
shadowColor: WidgetStateColor.transparent,
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
Theme.of(context).colorScheme.primaryContainer
|
||||
),
|
||||
onSubmitted: (value) => context.go(Uri(
|
||||
path: SearchPage.route,
|
||||
queryParameters: {'query': value}).toString()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, suggestionsBuilder:
|
||||
(BuildContext context, SearchController controller) {
|
||||
return [Text("dadada")];
|
||||
}),
|
||||
FutureBuilder(
|
||||
future: APIs.isLoggedIn(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data == true) {
|
||||
return MenuAnchor(
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.exit_to_app),
|
||||
child: const Text("登出"),
|
||||
onPressed: () async {
|
||||
final SharedPreferences prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.remove('token');
|
||||
if (context.mounted) {
|
||||
context.go(LoginScreen.route);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
builder: (context, controller, child) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.account_circle),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}, suggestionsBuilder:
|
||||
(BuildContext context, SearchController controller) {
|
||||
return [Text("dadada")];
|
||||
}),
|
||||
FutureBuilder(
|
||||
future: APIs.isLoggedIn(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data == true) {
|
||||
return MenuAnchor(
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.exit_to_app),
|
||||
child: const Text("登出"),
|
||||
onPressed: () async {
|
||||
final SharedPreferences prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.remove('token');
|
||||
if (context.mounted) {
|
||||
context.go(LoginScreen.route);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
builder: (context, controller, child) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.account_circle),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
})
|
||||
],
|
||||
),
|
||||
useDrawer: false,
|
||||
selectedIndex: _selectedTab,
|
||||
onSelectedIndexChange: (int index) {
|
||||
@@ -223,6 +228,8 @@ class _MainSkeletonState extends State<MainSkeleton> {
|
||||
context.go(ActivityPage.route);
|
||||
} else if (index == 3) {
|
||||
context.go(SystemSettingsPage.route);
|
||||
} else if (index == 4) {
|
||||
context.go(SystemPage.route);
|
||||
}
|
||||
},
|
||||
destinations: const <NavigationDestination>[
|
||||
@@ -242,6 +249,10 @@ class _MainSkeletonState extends State<MainSkeleton> {
|
||||
icon: Icon(Icons.settings),
|
||||
label: '设置',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.computer_rounded),
|
||||
label: '系统',
|
||||
),
|
||||
],
|
||||
body: (context) => widget.body,
|
||||
// Define a default secondaryBody.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ui/activity.dart';
|
||||
import 'package:ui/system_settings.dart';
|
||||
import 'package:ui/settings.dart';
|
||||
import 'package:ui/welcome_page.dart';
|
||||
|
||||
class NavDrawer extends StatefulWidget {
|
||||
|
||||
@@ -29,6 +29,9 @@ class APIs {
|
||||
static final activityUrl = "$_baseUrl/api/v1/activity/";
|
||||
static final activityMediaUrl = "$_baseUrl/api/v1/activity/media/";
|
||||
static final imagesUrl = "$_baseUrl/api/v1/img";
|
||||
static final logsBaseUrl = "$_baseUrl/api/v1/logs/";
|
||||
static final logFilesUrl = "$_baseUrl/api/v1/setting/logfiles";
|
||||
static final aboutUrl = "$_baseUrl/api/v1/setting/about";
|
||||
|
||||
static final tmdbImgBaseUrl = "$_baseUrl/api/v1/posters";
|
||||
|
||||
|
||||
@@ -292,3 +292,65 @@ class Storage {
|
||||
"default": isDefault,
|
||||
};
|
||||
}
|
||||
|
||||
final logFileDataProvider = FutureProvider.autoDispose((ref) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.logFilesUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
List<LogFile> favList = List.empty(growable: true);
|
||||
for (var item in sp.data as List) {
|
||||
var tv = LogFile.fromJson(item);
|
||||
favList.add(tv);
|
||||
}
|
||||
return favList;
|
||||
});
|
||||
|
||||
final aboutDataProvider = FutureProvider.autoDispose((ref) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.aboutUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
return About.fromJson(sp.data);
|
||||
});
|
||||
|
||||
class LogFile {
|
||||
String? name;
|
||||
int? size;
|
||||
|
||||
LogFile({this.name, this.size});
|
||||
|
||||
factory LogFile.fromJson(Map<String, dynamic> json1) {
|
||||
return LogFile(name: json1["name"], size: json1["size"]);
|
||||
}
|
||||
}
|
||||
|
||||
class About {
|
||||
About({
|
||||
required this.chatGroup,
|
||||
required this.goVersion,
|
||||
required this.homepage,
|
||||
required this.intro,
|
||||
required this.uptime,
|
||||
});
|
||||
|
||||
final String? chatGroup;
|
||||
final String? goVersion;
|
||||
final String? homepage;
|
||||
final String? intro;
|
||||
final Duration? uptime;
|
||||
|
||||
factory About.fromJson(Map<String, dynamic> json) {
|
||||
return About(
|
||||
chatGroup: json["chat_group"],
|
||||
goVersion: json["go_version"],
|
||||
homepage: json["homepage"],
|
||||
intro: json["intro"],
|
||||
uptime: Duration(microseconds: (json["uptime"]/1000.0 as double).round()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
136
ui/lib/system_page.dart
Normal file
136
ui/lib/system_page.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
|
||||
import 'package:ui/providers/settings.dart';
|
||||
import 'package:ui/widgets/progress_indicator.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SystemPage extends ConsumerStatefulWidget {
|
||||
static const route = "/system";
|
||||
|
||||
const SystemPage({super.key});
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() {
|
||||
return _SystemPageState();
|
||||
}
|
||||
}
|
||||
|
||||
class _SystemPageState extends ConsumerState<SystemPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final logs = ref.watch(logFileDataProvider);
|
||||
final about = ref.watch(aboutDataProvider);
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ExpansionTile(
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
initiallyExpanded: true,
|
||||
childrenPadding: EdgeInsets.all(20),
|
||||
title: Text("日志"),
|
||||
children: [
|
||||
logs.when(
|
||||
data: (list) {
|
||||
return DataTable(
|
||||
columns: const [
|
||||
DataColumn(label: Text("日志")),
|
||||
DataColumn(label: Text("大小")),
|
||||
DataColumn(label: Text("*"))
|
||||
],
|
||||
rows: List.generate(list.length, (i) {
|
||||
final item = list[i];
|
||||
final uri =
|
||||
Uri.parse("${APIs.logsBaseUrl}${item.name}");
|
||||
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(item.name ?? "")),
|
||||
DataCell(Text("${item.size ?? 0}")),
|
||||
DataCell(InkWell(
|
||||
child: Icon(Icons.download),
|
||||
onTap: () => launchUrl(uri,
|
||||
webViewConfiguration: WebViewConfiguration(
|
||||
headers: APIs.authHeaders)),
|
||||
))
|
||||
]);
|
||||
}));
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
loading: () => const MyProgressIndicator())
|
||||
],
|
||||
),
|
||||
ExpansionTile(
|
||||
title: Text("关于"),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.center,
|
||||
initiallyExpanded: true,
|
||||
children: [
|
||||
about.when(
|
||||
data: (v) {
|
||||
final uri = Uri.parse(v.chatGroup ?? "");
|
||||
final homepage = Uri.parse(v.homepage ?? "");
|
||||
return Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
"#",
|
||||
style: TextStyle(height: 2.5),
|
||||
),
|
||||
Text("主页", style: TextStyle(height: 2.5)),
|
||||
Text("讨论组", style: TextStyle(height: 2.5)),
|
||||
Text("go version", style: TextStyle(height: 2.5)),
|
||||
Text("uptime", style: TextStyle(height: 2.5)),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
)),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(v.intro ?? "",
|
||||
style: const TextStyle(height: 2.5)),
|
||||
InkWell(
|
||||
child: Text(v.homepage ?? "",
|
||||
style: const TextStyle(height: 2.5)),
|
||||
onTap: () => launchUrl(homepage),
|
||||
),
|
||||
InkWell(
|
||||
child: const Text("Telegram",
|
||||
style: TextStyle(height: 2.5)),
|
||||
onTap: () => launchUrl(uri),
|
||||
),
|
||||
Text("${v.goVersion}",
|
||||
style: const TextStyle(height: 2.5)),
|
||||
Text("${v.uptime}",
|
||||
style: const TextStyle(height: 2.5)),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
loading: () => const MyProgressIndicator())
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -539,7 +539,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
url_launcher:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
|
||||
@@ -46,6 +46,7 @@ dependencies:
|
||||
flutter_adaptive_scaffold: ^0.1.11+1
|
||||
flutter_form_builder: ^9.3.0
|
||||
form_builder_validators: ^11.0.0
|
||||
url_launcher: ^6.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user